The “I’m stupid” elm language nugget #17

tldr; Hosting CodeMirror in elm apps seamlessly.

I recently released https://github.com/prozacchiwawa/elm-lightbox/ , which I’m thinking at some time later, I might package up in a slightly modified form as an atom package. It isn’t 100% unique (similar things exist in the form of chrome extensions), but this one is mine, and I like having as a standalone page that does its own quick refresh. In gaining more web skills over the past year, I’ve found myself wanting to just concentrate on precise alignment between a design image and layout at times, and tried this in a few different forms. I think what I came up with distills the right things and has a pleasant feel to use.

This post however is about how CodeMirror integrates in elm-lightbox.

I used a technique I’ve used before in elm, to bind some properties on Node that allow elm to communicate with the DOM. Specifically, property setters go off in the context of DOM construction, that is the value is set (therefore the function invoked) after the DOM node is created (with a caveat I hadn’t considered yet, detailed below). Property getters go off in the context of event handlers, therefore they are downstream from the context in which events are fired, and they’re suitable for actions that must be taken in the context of end user actions, such as mouse clicks.

In the case of CodeMirror, there’s a convenient fromTextArea factory that takes over a textarea DOM element and places something fairly compatible in its place. Using that in a function that runs at DOM construction time almost works:

function enableCodeMirror(v) { 
console.log(‘enableCodeMirror’,this);
if (v && !this.codeMirror) {
if (this.parentNode) {
var self = this;
this.codeMirror = CodeMirror.fromTextArea(this);
this.codeMirror.on(‘change’, function(doc, change) {
var evt = new CustomEvent(
‘__arty__change’,
{ detail: self.codeMirror.getDoc().getValue() }
);
self.dispatchEvent(evt);
});
} else {
var self = this;
setTimeout(
function() { enableCodeMirror.apply(self, [v]); },
100);
}
}
}

There’s one thing to notice here, the check for this.parentNode at line 4 is determining whether the DOM node has been parented to the primary DOM yet, because CodeMirror will make the assumption that the node exists in the primary DOM. Here, I retry it on a timer (so lame …). I could (and probably should) use some other event binding for this.

Other than that, we’re creating a custom event on the DOM node called __arty__change that acts similarly to “change” (mainly to ensure that I’m fully in control of the events being fired), and listen to that in elm. I haven’t seen custom events discussed much in elm circles, but they do the right thing.

function showCodeMirror(v) { 
if (this.codeMirror) {
if (v) {
this.codeMirror.getDoc().setValue(this.value);
this.codeMirror.refresh();
}
}
}
Object.defineProperty(Node.prototype, ‘__arty__visible’, {
set: showCodeMirror
});
Object.defineProperty(Node.prototype, ‘__arty__enableCodeMirror’, {
set: enableCodeMirror
})

The code for showCodeMirror is a bit tricky, so I’ll mention it specifically: CodeMirror uses the attributes of the real DOM element to set up its container for the actual code control, so if you use fromTextArea on a hidden or unusually positioned textarea, then the CodeMirror control might not render properly. I did this hack where the property value set by elm tracks whether the textarea is rendered visible or not. Virtualdom will reset this property when this part of the dom changes, so the check for v is mostly telling us whether elm has recently changed the visible status set on this property from false to true. In that case, the code ensures that the CodeMirror control has the latest version of the “value” property on the control, then calls its all-purpose “refresh” method to cause it to create its rows and such.

Note: I realized while writing this that I’m making the assumption that elm hasn’t change the value it gives the textarea and transitioned it to visible at the same time here (although it might work, there’s no guarantee about the order in which the properties will be assigned). One way to fix that would be to pass either the text value or null to __arty__visible and use that value.

In elm, using this is fairly normal:

div [ addIfSelected Layout [SelView] [Fill] ] 
[ Html.textarea
[ c.class [FillText]
, HA.property “value” (JE.string model.rawLayout)
, HA.property “__arty__enableCodeMirror” (JE.bool True)
, HA.property “__arty__visible”
(JE.bool (Layout == model.selectedView))
, Html.Events.on “__arty__change”
(JD.field “detail” JD.string |> JD.map SetLayout)
] []

And it works pretty well.