How to display server-rendered HTML in Elm 0.19

I’m building a team communication product called Level, and one of the core features is rendering posts as Markdown.

Elm has a library to handle this on the client-side. It’s a wrapper around the marked.js library that uses Kernel code call out to JavaScript land. (It appears the plan is to implement a native Markdown rendered in Elm eventually).

This library works great for doing basic Markdown rendering without additional decoration but is a bit limited if you need to do things like traverse the rendered HTML to inject special styles, or perform additional sanitization of user input.

To my knowledge, there’s not an API to traverse and modify DOM trees in Elm. It might be possible to accomplish these things on the client-side with enough JavaScript hackery, but that admittedly makes me a bit squeamish.

Before upgrading to 0.19, there was a hack for taking some server-rendered HTML and injecting it into the DOM. It looked something like this:

div [ property "innerHTML" (Encode.string rawHtml) ] []

Setting innerHTML in this way disallowed in 0.19 for security reasons, so it was back to the drawing board!

Luke Westby’s talk at elm-conf inspired me to take a closer look at using custom elements for this job. After all, a custom component is essentially a custom DOM node type that is composed of your own logic and whatever data you pass to it. The solution turned out to be pretty elegant.

I’ll mention an important disclaimer now: you should let Elm handle DOM rendering if at all possible. I have not fully sussed out the performance implications of this workaround, but I suspect there is a penalty (albeit one I’m willing to pay for the sake of getting my product’s alpha version finished). And, you definitely should not use this unless the markup has been sanitized.

The web component code looks like this. You should import this module before instantiating any custom elements in Elm:

customElements.define(
  "rendered-html",
  class RenderedHtml extends HTMLElement {
    constructor() {
      super();
      this._content = "";
    }

    set content(value) {
      if (this._content === value) return;
      this._content = value;
      this.innerHTML = value;
    }

    get content() {
      return this._content;
    }
  }
);

The Elm side looks like this:

import Html exposing (Html)
import Html.Attributes exposing (property)
import Json.Encode as Encode


postBody : String -> Html msg
postBody html =
    Html.node "rendered-html"
        [ property "content" (Encode.string html) ]
        []

You’ll want to include the @webcomponents/custom-elements polyfill for browsers that don’t yet fully support custom elements.

If you are using Babel < 7.0 for transpiling, then you may need to use a shim to support extending HTMLElement:

import "@webcomponents/custom-elements/src/native-shim"

And that’s it!

 
15
Kudos
 
15
Kudos

Now read this

Why Phoenix.LiveView is a big deal

Last week at ElixirConf, Chris McCord announced a new project called Phoenix.LiveView. I believe this library has the potential to reshape the way many developers build reactive user interfaces on the web. Today, Phoenix provides a... Continue →