Projectional Web Editors with Elm

As I’ve mentioned before, I believe The Elm Architecture (TEA) is a particularly good fit to implement projectional editors for the web.
So I’ve used the Elm programming language, which has TEA built-in, to create a little prototype I wanted to demonstrate.

The main goal of this was to find a setup which is easy to use even for people not familiar with Elm, to allow them to try it out. It’s by no means complete or stable, so don’t expect wonders.

What’s there?
There exist three Elm modules (a module is a compilation unit in Elm) that guide you along building projectional editors. The repository can be found here.

  • Runtime: Allows you to set up a “Elm projection program”
  • Structure: Contains the central type (“Node”) to built Elm projections, as well as functions to build models.
  • Editor: Contains an API to transform domain models into cell models, which get eventually rendered in HTML; it also encapsulates common behavior from your domain, like
    • adding/deleting domain nodes and properties (especially updating nested structures, which requires a lot of boilerplate in Elm)
    • automatically updating selections (still pretty incomplete and buggy)

What’s bad?
A lot of things, believe me, but one thing I wanted to mention explicitly is that, by using the common type “Node” as the main building block for everything looses a lot of the nice type-safety in Elm.
Also, the code’s bad, so don’t look at it if can avoid it :stuck_out_tongue:
Naturally, everything is subject to change, so don’t rely on anything that’s there.

Example
Let’s look how one can build an editor for a (very basic) state machine language like this:

There is no code generation, scoping, type checking, or anything like that. It’s just a model and it’s projection in Html.

The Elm code to build a web editor like that is currently best divided into four part, three of which are very simple. So we will look at the source code step-by-step, starting with a necessity:

Necessity - imports

import Structure exposing (..)
import Editor exposing (..)
import Runtime exposing (Model, projection)

Elm requires you to import modules you want to use. This can be done as shown above. The little (..) after the exposing keyword means we are lazy and make everything that is exposed by a module available in our module. We can (and actually always should) list everything we use from a module, like shown for the import of Runtime, where all we use is the Model type and projection function.

Step 1 - Domain variants
Elm features custom types similar to union types. In order build domain trees, we need to define a “domain type” which lists all type variants of our domain:

type Domain
    = StateMachine
    | Event
    | State
    | Transition

The type is called Domain and there are four “entities” or “concepts” in our language: StateMachine, Event, State, and Transition. I might use the term “variant” to refer to those sometimes.
Notice that we don’t make any assumption on how these are connected. The structure will be built rather implicitly in Step 4.

Step 2 - Program setup

main =
    projection initStateMachine editor

We use the projection function of the Runtime module to set up our Elm program. It expects two arguments:

  • an initial version of our domain model: that’s initStateMachine
  • a transformation function that takes in our domain model and produces a so-called “cell model”: called editor here

Notice that I deliberatly omitted the explicit type signature of the main function, since I believe that it would reather confuse you than help you. In general, it is a good practice to add explicit type signatures to top-level values and functions, even though Elm does not need them since it figures out all types by itslef.

Step 3 - Initial Domain Model

initStateMachine : Node Domain
initStateMachine =
    createRoot StateMachine

We define the value initStateMachine as a root node of variant StateMachine. Naturally, in the future we want to be able to save our models and load them again, but this is out of scope for this prototype.

Step 4 - Domain Model to Cell Model transformation, aka "Editor"

The current architecture looks like this:

While the blue and black stuff is taken care of, we are in charge of adding the green, which includes the transformation from domain model to cell model, i.e. a description of the editor.

I tried to create a very declarative API to do that.

editor : Node Domain -> Node (Cell Domain)
editor stateMachine =
    createRootCell
        |> with (editorStateMachine stateMachine)

This is our main transformation function we pass on to the projection function in Step 2. Notice the type signature at the very top: it takes a Node Domain and produces a Node (Cell Domain). If you are familiar with generic types in other languages, you can see that as argument of type Node<Domain> and a return type of Node<Cell<Domain>>.
The fact that the Cell type currently is tagged with our Domain type is an implementation detail I plan to get rid of, so that this should become editor : Node Domain -> Node Cell at some point.

Anyway, we create a root cell and use Elm’s pipe operator (|>) to pass on the root cell to the Editor.with function. It means that whatever is produced by the first argument, e.g. the function call editorStateMachine stateMachine gets added to our root. Let’s look at that function next.

editorStateMachine : Node Domain -> Node (Cell Domain)
editorStateMachine sm =
    vertStackCell
        |> with (editorStateMachineName sm)
        |> with (editorEvents sm)
        |> with (editorStates sm)

The editorStateMachine function creates a “vertical stack cell”, which is enhanced by three “sub-editors”, which we will tackle one-by-one:

editorStateMachineName : Node Domain -> Node (Cell Domain)
editorStateMachineName sm =
    horizStackCell
        |> with (constantCell "name:")
        |> with (inputCell "name" sm)
        |> addMargin Bottom 20

This represents “the first line” of our editor, hence a horizontal stack cell as a container. It contains a constant of value “name:” and a input cell for a property called “name” on the state machine node. We also add 20 pixels margin below this sub-editor.

editorEvents : Node Domain -> Node (Cell Domain)
editorEvents sm =
    let
        editorEventsResult =
            case getUnderCustom "events" sm of
                [] ->
                    [ editorEventPlaceholder sm ]

                events ->
                    List.map editorEvent events
    in
        vertStackCell
            |> with (constantCell "events")
            |> with
                (vertStackCell
                    |> addIndent
                    |> withRange editorEventsResult
                )
            |> with (constantCell "end")
            |> addMargin Bottom 20

This is a bit more involved and I have ideas to simplify it.
Let’s focus on the bottom part first. We want to describe a vertical stack of cells that starts with the events keyword and ends with the end keyword.

...
vertStackCell
	|> with (constantCell "events")
	...
	|> with (constantCell "end")
...

In between, we want to see all Events of our domain, or show a placeholder cell that allows us to add a first event. We wrap either result in its own vertical stack, so we can have it easily indented. withRange allows us to add lists of editors:

...
(vertStackCell
	|> addIndent
	|> withRange editorEventsResult
)
...

We define a local value called editorEventsResult which makes the decision what to show.
First, It looks for events under the state machine node.

getUnderCustom "events" sm

Notice that this means we expect our events to live under a custom “feature” called "events". Each Node can have children either under a default feature, or a labeled, custom feature, like the one we are using here. The rationale is that states are the “main thing” of a state machine and should therefore live under its “default” feature (we will see this below), whereas everything else should be grouped under custom features, like events should live together, under the "events" feature.

If the lookup of children under “events” is empty (denoted by the first case match using a Empty List Literal: [] ), we produce a placeholder cell.
If there are events, we map them over the editorEvent function to produce a list of editors (i.e. a list of type Node (Cell Domain)).

Before we look at the placeholder editor for events, let’s look at the editor for states, since it is very similar to the one for events.

editorStates : Node Domain -> Node (Cell Domain)
editorStates sm =
    let
        editorStatesResult =
            case getUnderDefault sm of
                [] ->
                    [ editorStatesPlaceholder sm ]

                states ->
                    List.map editorState states
    in
        vertStackCell
            |> withRange editorStatesResult

Again, we decide whether to show a placeholder or the list of states by means of a local value (editorStatesResult). However, this time, we look under the state machine’s default feature for state children, this was our design decision for our state machine models.

Now, let’s see the placeholder editors. They introduce explicit effects:

editorEventPlaceholder : Node Domain -> Node (Cell Domain)
editorEventPlaceholder sm =
    placeholderCell "no events"
        |> withEffect (replacementEffect "events" sm ctorEvent)
		

ctorEvent : Node Domain
ctorEvent =
    createNode Event
        |> addText "name" ""


editorStatesPlaceholder : Node Domain -> Node (Cell Domain)
editorStatesPlaceholder sm =
    placeholderCell "no states"
        |> withEffect (replacementEffect "" sm ctorState)
		

ctorState : Node Domain
ctorState =
    createNode State
        |> addText "name" ""

The Editor.placeholderCell function allows us to produce a placeholder cell with a default text.
We explicitly have to add a certain “effect” here to make it work, using the Editor.withEffect function. This way, we tell the system what to do when the user presses “Enter” whilst the placeholder cell is focussed. Currently, the effect will add a “first” child under the given feature to the given node.
In case of the event placeholder, we want to add an event node under the "events" feature of the state machine node sm.

The function ctorEvent descibes how a new event looks like.
Notice that this function uses the Structure API to build a domain node, whilst our editor functions use the Editor API to build cell nodes!

The editorState function uses the same patterns as we’ve seen above, but it introduces the usage of a new cell kind (buttonCell) and a new effect kind (insertionEffect). Feel free to check it out.

And let me know what you think!

4 Likes

Interesting! need some more time to read all the details.

I know it’s a LOT, so feel free to ask questions if my blabbering is incomprehensive and/or overwhelming.

1 Like

Quite impressive! You have achieved a lot in a relatively short time, and you explained it quite well.

I also spent a few days working on a prototype of a projectional web editor for a client and another one in free time.

My first thought is that there is a lot of work to be done to make it usable. For example, to build the navigation between cells right or to support autocompletion in a nice way.

What I am doing right now is using input cells for everything:

  • If they are constant they are not editable, still I can traverse them when navigating with arrows and I could do some actions on them, like deleting or adding elements by pressing backspace or enter
  • If they represent editable values, like name, I can edit them normally
  • If they represent relation I can have autocomplete on them

All these inputs automatically resize to fit the content. My experiments are much less advanced than yours, but I think we could potentially share some work on the UI.

Here you can see a simple demo:

Also on the editing experience, the opinion of @jos.warmer could be very valuable, given his work on ProjectIt.

In general I hope to see many different experiments to popup over time. I think it could be great if we could reduce the cost of experimenting by sharing components like the active repository suggested by Markus or a UI library for building usable editors.

2 Likes

Yes, I saw that on Twitter and meant to ask you about it.

I wouldn’t know which of the two approaches is more advanced, you managed to incorporate editable dropdowns, which I only have on a conceptual level.
Are those HTML input elements with datalists attached? That 's what I am planning to do. My constants are HTML labels at the moment, I have to look if they can be made “selectable” (I guess they are focusable in the DOM and I could dynamically draw a border or somehting to visualize the focus, but that’s not top on my list right now). I’m more concerned with a relatively nice API and a somewhat general solution that could potentially scale to somehting more.

I agree with your sentiments, so please share a bit more how you created your prototype. Any third party libs worth mentioning? Is the source available somewhere?

Thanks!

I did not even know that you could attach datalists to input elements!

I am using a small library for autocompletion (it is named… autocomplete.js). A lot of stuff is currently hacked together, I do not have a nice conceptual model as yours. Right now I want just to understand if usability could be decent enough to make this usable at all.

Right now the code is available because I have hacked the repository containing the examples I wrote for an article, I just created a set of branches as I was experimenting. The repository is:GitHub - Strumenta/calc-monaco-editor: A browser based editor for a simple DSL to perform calculations . The code on master is the one I wrote for the article, while the code with these experiments is in other branches. It is beyond messy :slight_smile:

At some point I plan to make it understandable and perhaps write a short article about it. Hearing about your experiment I was thinking to use some virtual dom library. I just started looking into this and I have a very limited understanding of this stuff. Incidentally that made me realize what was all the fuss around “react” as I had no idea how it worked, but having to understand virtual dom I ended up reading about react

2 Likes

Hi Federico,

thanks for the link, I don’t really “speak” JS, so every time I look at that language I realize there are some synapses missing in my brain in order to understand what’s going on.

I think it is good that your focus is different, so that we might be able to put together lessons learned at some point.

What specific usability questions do you have in mind? Cause I think, with all the options HTML5 offers, of course one CAN make something usable. The intentional platform, while it started out as a language workbench, eventually became a application workbench/platform, since they realized that you can use the approach of building languages pretty much to build applications as well. So the UIs eventually were not limited to typical “cell-based” structured editors, but you could pretty much declare any kind of UI and application.
Even though I started out with basic cell building blocks like vertical and horizontal stacks, constants, and input cells, the approach scales and you can (already) use buttons, and eventually once can imagine a widget layer, SVGs, Ink, etc. Not saying I’m going to build something like this, but conceptually it is possible.

Yes, same here. I had a basic understanding by the term alone and by the fact that we had something very similar at ISC. I can recommend this part of the Elm guide to learn about their virtual DOM and how one can optimize the defaults using Html.lazy and Html.keyed (next page). However, the information on the virtual DOM topic is very basic and you certainly won’t learn a lot of new things given that you already looked into React.

Just cause I really like this project and it is a great demonstration of what can be built (by a single person), I wanted to share the Kite project. There is a very short demo video online.
“An Interactive Visualization Tool for Graph Theory written in Elm.” You can try it out yourself, I think its fun, not least cause the animations are so fluid.

1 Like

Things to check out over the next 10 days - thanks, gentlemen!

Hi,

Looking at your experiment, I see that the basic architecture is very similar to ProjectIt. In ProjectIt you define the projection in terms of “boxes”, which can be combined in various ways (currently horizontal, vertical, grid like). This will then be mapped to HTML (using React) by ProjecIt. Automatic updates in the projection are fully dealt with by Mobx.
User actions on the Web page are translated to actions on boxes by ProjectIt, this way the developer can define all behavior on the Box level, without having to deal with HTML.

My general experience is that the projection of a projectional editor is relatively easy. The interaction with the user is the really challenging (and fun!) part. There isn’t much experience (mainly from using MPS) or theory to fall back on, so it is your own creativity and experimentation that counts.

Frederico is quite right about the usability, this is the major issue for projectional editors. The structure can very quickly make things cumbersome to use.

Let me give one example of a usability problem for a complex interaction that we did in ProjectIt: editing expressions. The goal is to enable users of the editor to simply type something like:

987 + 12 * 234 + 111

and at any time during typing always have the expected AST. In MPS you would use the grammarcells plugin from mbeddr to achieve this. ProjecIt has support for typing expressions like this built-in. In this case several things are interesting:

  1. While you type, the AST (model) on the background needs to be continuously restructured, for example:

    • user types 987, AST is a number literal node
    • user types +, restructuring: 987 needs to become child of the + node. AST is now a + node with the 987 as a child (
    • user types 12. AST is a + node with 987 as left and 12 as right child
    • user types *, restructuring: the 12 child of the + node needs to be replaced with the new * node, and the 12 should becomes the left child of this * node. AST is now a + node

    Etc. etc. his needs to happen with almost any operator you type, depending on its priority. And of course the user should also be able to type any operator between a number literal and an operator, e.g typing a * between the 987 and the +.

  2. Secondly when the focus at the end of the input box for the number literal (e.g. 12) and I type the * symbol, the typical behavior is to give an error that this is not a valid number. The user then first needs to go to the next cell to be able to type *. Cumbersome!
    Instead, ProjectIt will see that the * does not fit in the number box, and try to ‘type’ it (virtually) in the next box. There it can be recognized as an operator and the user can continue typing.

It also is rather interesting to take a look at the backspace key. Ideally the user should be able to type the expression from left-to-right, and then backspace from right-to-left and get back where he started. It is a challenge to make this work, ProjectIt does this partly, but a projectional editor web that I have built for Mendix does it all the way.

It is lots interaction issues like the above, including all of their combinations that makes the difference between usable and unusable.

ProjectIt has been going slowly last year, but this year there is a new collaborator with enough time and things are speeding up. The development version of ProjectIt already includes a meta model definition language, and a scoping definition language. From this we generate all the TypeScript code for the model and a default projection for the language. You can then redefine one or more projections yourself in TypeScript (in a declarative way) if you don’t like the generated default.
In addition we are working on an editor definition language, to make it easier to define your own projections. A validator and typer are also in the making.

I know this is a lot of information again to get through (sorry @angelo ), but I do like the in-depth explanation / discussion.

Jos

3 Likes

Hi Jos,

thanks for you message.

Personally, I’m not very interested in the problem space of how we can built a projectional editor for expressions in a way that it essentially feels and behaves like a text editor. At Intentional, I was part of a group where we basically did exactly that, namely allow to incorporate “projectional text editing with incremental parsing on each keystroke” into the Intentional platform (can’t go into details).

I know that–from a practical point of view–many languages feature some sort of need for expressions, so I don’t want to discourage you or anybody else from working on this stuff, but from a language engineer point of view, I always felt like I’m doing something wrong when I put days and weeks into trying to built usable projectional editors for expressions. It was clear from the get-go that the best possible result (usability-wise) would still be inferior to any decent text editor (I think for editing expression trees with infix operators in particular, text editors are just hard to beat).

I’m more interested in projectional editors for other domains, with an emphasis on “higher level notations” and corresponding data visualizations and manipulations. Also, I use this project as a means to get better in Elm.

It’s good to hear that you have more manpower nowadays, it certainly sounds like ProjetIt is on a good way.

How are your “language definition languages” built, if you don’t mind my asking? Are those parsed languages, or are you using ProjectIt to exercise some dogfooding?

One thing that might turn out to be a big detriment in my approach, and somehting I originally did not like at all but currently find somewhat liberating is that there is no “meta modelling” layer. You define your domain’s entities basically by naming them and then you define how your model is being built by means of describing the editor (and its effects). Again, there are some obvious downsides having the structure only implicitly defined like that, but I’ll roll with it and see how it goes.

Best,
Robert

Ill make it 20 days, @jos.warmer. Great to read these things! Maybe one day we’ll be able to set up the PEC - Projectional Editor Challenge :wink:

Hi Robert, I was using the expressions as an example of challenges in user interaction. For the simple example a text-editor/parser would work fine, but that approach is too limited in my opinion because it assumes that the notation is linear and textual.

My motivation for projectional editors is the fact that you are not restricted to text only, but can mix different notations. I focus especially on non-IT users for which I have found this to be crucial.

To continue expressions example, I would like to be able to replace the numeric literals by a mathematical Sum, or a decision table, or use a horizontal bar for division. The fact that you can do this is the USP for projectional editors. The following picture shows such an example:

Jos

Hi Jos,

I’m well aware of that, but adding mathematical symbols or horizontal bars to your example doesn’t change what I said earlier and what I experienced over the last couple of years. Generally, it is a great feature to be able to mix notations, I agree.
From a UX perspective, however, that doesn’t mean that one should mix editable notations arbitrarily just because it’s possible. And I think especially for the expression-based cases, like the one you suggest, the main value of the projection lies in its visualization, not the ability of directly manipulating the notation.
I guess what I’m saying is that I agree that the ability of mixing notations in general is good, but I’m not sure that focussing on it in the context of expression-based languages makes the most sense to me. If you have a specific use case where a UI like that is necessary, that might be different.

Hi Robert,

The expressions were just an example of showing interaction challenges, as far as I am concerned there is no focus on expressions at all.

You are arguing that the main value of the projection lies in visualization, but as the title of this thread is “Projectional Web Editors with Elm” I was focusing the discussion on editors, not on read-only visualizations.

But … looking at the larger picture there are many techniques that can be used:

  • Textual DSL’s, e.g. with Xtext
  • Visual DSL’s, e.g. created with GMF
  • Projectional DSL, e.g. done using MPS
  • Visualizations (read-only), I have done them with e.g. PlantUml
  • etc. …

I don’t see any of these being “better” than the others, they are different solutions for different problems. It fully depends on the context which one is the preferred choice. Context includes the kind of end-users, the technologal environment of the customer, the time and money available, expertise of the project members, etc. etc.

I have used all of these techniques in various projects, because of different circumstances. Several times I use more than one in the same project. E.g in my current main project we are using MPS for projectional editors, we have several read-only visualizations. but we also have parts that use a text based parsing approach. It is a big project and they all have their specific use case.

Sometimes people assume that I am a projectional evangelist, because I started ProjectIt. However, my main reason for starting ProjectIt was not that I think projectional is always better that anything else, but it was the lack of proper tooling for the projectional option, compared to all the others. I just want projectional to become an " equal opportunity" technology. And then everyone should choose what they think is best for them.

Jos

3 Likes

I was saying that “especially for the expression-based cases, like the one you suggest,” the main value lies in its visualization. The challenges that come with editing such expressions and allowing infix notation in a projectional editor are very specific, which I don’t feel like are worth the effort required to properly address them.
I feel like there is a lot of misunderstanding and misassumptions happening in this discussion, so I’d prefer to not continue it, it’s not going anywhere productive.

Hi Robert,

I agree, let’s leave it at this.

I’ve created a video showing some more progress. I looked at multiple projections, particularly a graphical one, during the last few days:

Notice that it might take some more minutes until HD is available.

What do you think?

2 Likes

Quite impressive! I will ask something stupid, but is everything written in Elm or do you have parts in JS, or CSS? In particular all the presentation details, the navigation with the arrow keys, and the way you do autocomplete are things that you handle in Elm? Do you think those parts could be potentially reusable?

That are fair questions.

This is 100% written in Elm at the moment, and I intend to keep it that way. I make use of some Elm packages, especially with the geometry and the force logic for layout, I use a third party elm package, but I try to keep third party packages to a minimum.

The one exception is a tiny css file with some style classes (just for handling split screens), but that’s just for my convenience at the moment.

In particular all the presentation details, the navigation with the arrow keys, and the way you do autocomplete are things that you handle in Elm?

Yes, all in Elm. The idea is to implement a functional reactive architecture very principled. So, every time the user does something in the UI, it might produce an “effect”. Either, because a editor cell already has an effect handling built-in (e.g. arrow key navigation and keyboard input I’ve built into the “InputCell”), or because the user declared an effect on a cell (e.g. for inserting and deleting elements in a sequence). I show how one can add these effects briefly in the video.

The Editor module decides if it can handle the effect by itself, or if it needs to defer to the user domain. One example of a effect that allows the language engineer to hook-up custom behavior is the “InsertionEffect”. It is necessary that the language designer defines how a new node that gets inserted by the user is constructed, since this is domain-specific. Think “Node Factories” in MPS.

Everything else you see in the video is handled by the “platform”, and therefore reusable for each and every domain you want to implement in said platform. The State Machine example is less then 250 lines of Elm code, and Elm code tends to be very vertical, so it’s really not a lot of code one has to write.

I try to make the API slim, while the platform itself becomes thick, so that defining new domains is very simple. However, this means that I make some strong design assumptions at the beginning of building this platform on how things behave and look. For example, a Vertex in a graph can exactly have one input field at the moment. This is obviously bad, but something I intend to fix so that vertices are more flexible, regarding their content. The whole graph thing was just to play around with SVGs and how to dynamically draw them.

Another good example is the “scoping” effect mechanism. At the moment, the user defines a “refCell” like so:

E.refCell MyTargetVariant (Role "ref") nodeContext Nothing

This means this cell references a domain node of variant MyTargetVariant (think: MPS concept). It has a role and nodeContext, so the Editor module knows where to read/write the name of the referenced thing (currently references work by “name”) into the domain model once the user types inside the reference cell.
The last argument is a Maybe. If the user decides to pass in Nothing, this means the global scope will be used by the CreateScopeEffect, which is implicitly added to a reference cell. However, you can also provide your own scope provider instead of Nothing, if you wish. The effect itself is triggered, i.e. the scope is calculated every time a reference cell is focused.

The rule of thumb is: the more functionality I can get working for my examples (I have several, not just the state machine one) without changing the example code itself, but by enriching the platform, the better.

So, if you were asking to reuse this outside of Elm: I’m not planning on any JS interop, no.