Writing custom-elements for elm
Tags: elm, customelements, javascript, ux
Published: 2020-11-01 15:37:29

---
title: Writing custom-elements for elm
published: true
description: Quick introduction and explanation of how custom elements are playing nicely with Elm and how to write your owns.
tags: elm, customelements, javascript, UX
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/afmjyjpz8nsb2lob8k9l.png
---

**Summary:** There are 2 options to integrate JavaScript and Elm, one is the port system which has been around for a while, the other is to use custom-elements.
In this post, we'll see that it is rather simple and show 2 examples of packages that use it.

The introduction is a tad long but you can always just skip to the [main part](#making-it-work).

## What's custom-elements?

Custom elements are part of the Web Components and in short, it allows us to create new HTML tag that has a set of behaviour defined in JavaScript.
Think of it as a "super-tiny-application-wrapped-in-a-tag".

Have you ever wanted to define a small thing that you could call like `<drawing-board tool="pencil" thickness="10pt"></drawing-board>` and get the whole set of features that goes with it?
Well, custom-elements allows you to do just that.

When you think of it, inputs in general and `<textarea>` in particular encompass a lot of features and "state" to know what the user input is, where is the cursor, if there is some auto-completion available, ...

Custom-elements are just a neat way of defining your own version of that.

For a more complete look at custom-element, you can refer to this post:
{% link https://dev.to/jamesrweb/an-introduction-to-custom-elements-5327 %}

or refer to the GREAT and all-mighty MDN: [Using custom elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements)

## How does this help us with Elm?

Quick words of introduction if you don't know [Elm](https://elm-lang.org/): Elm is a functional language designed for the front-end.
Think of it as a "light" and more friendly version of Haskell repurposed for a single task.

Among many advantages, Elm ensures that once compiled your code won't generate any runtime errors.
One of the ways to do this is to force the code your write to handle all the different way things can go wrong using constructs such as `Result` or `Maybe` which works just perfectly.

All of this is a long introduction to say that to provide you with this guarantee, Elm restrains the interactions with the unsafe world outside (a.k.a the JavaScript Doomdom...).
Traditionally most interactions are handled in something called [ports](https://guide.elm-lang.org/interop/ports.html).
The main interest of exchanging information between the outside world and elm via ports is that you can be sure of preserving the integrity of your elm code.

Custom elements, however, are an interesting way of integrating some isolated JavaScript in your elm codebase.
This covers for instance: charting libraries, chatbots, ...

Yes, yes, good, how does that work then? Well, let's get to it.


## Making it work

The elm documentation provides an [excellent base](https://guide.elm-lang.org/interop/custom_elements.html) to start interoperating custom elements with elm.
However, nothing is better than a [~~shameless plug~~](https://github.com/leojpod/elm-keyboard-shortcut/) detailed example.

One thing I often found myself doing in elm in the various project I've worked on is a way to trigger some action based on keyboard events (or rather a combination of keys).
In the past, I had mostly used events from the [`elm/browser` package](https://package.elm-lang.org/packages/elm/browser/latest/Browser-Events#onKeyPress) which worked well but there were some drawbacks (for details on that, you can refer to [this link](https://github.com/leojpod/elm-keyboard-shortcut#why)).

Making a custom element to listen to a specific set of shortcut allowed me to keep things simple in my views and treat the shortcut as any other inputs.
Using this small package, I can make a dismissible modal like this:

```elm
shortcutModal : List (Html Msg) -> Html Msg
shortcutModal =
    Shortcut.shortcutElement
        [ Shortcut.esc CloseModal ]
        [ class "fixed top-0 bottom-0 left-0 right-0 flex flex-col items-center justify-center bg-gray-500 bg-opacity-75" ]
        << List.singleton
        << div [ class "w-3/4 max-w-4xl p-12 bg-white border-gray-800 rounded-lg shadow-xl" ]
```

If you look a bit closer to that piece of code you'll see the 2 key lines here:

```elm
    Shortcut.shortcutElement -- simply a wrapper for Html.node "shortcut-element"
        [ Shortcut.esc CloseModal ] -- the shortcutElement expect a list of shortcut and Shortcut.esc is just a simple way to say "when the user press ESC send me a CloseModal message"
```

The main interest of this version compared to doing it with subscriptions and `Browser.Events` is mainly readability:
Now even small bits of the UI can have shortcut without requiring you to keep track of their visibility/state in your subscriptions and you can also read it directly in the view.

**Enough! Show me some code!**

The entire code is available [here](https://github.com/leojpod/elm-keyboard-shortcut/) but let's go through the principal components of this solution.

### Defining shortcuts

Shortcuts are an association of a message to send and a description of a key combination.
A key combination is a base key and some optional modifier.
Elm provides a nice way to do that with is called union types (if you come from TypeScript or the like, think of them as a super-powerful enum type) and record types (again, TypeScript people, think of it as a simple class without method only some properties).

In the end, the shortcut definition looks like this:

```elm
type alias Shortcut msg =
    { msg : msg
    , keyCombination :
        { baseKey : Key
        , alt : Maybe Bool
        , shift : Maybe Bool
        , ctrl : Maybe Bool
        , meta : Maybe Bool
        }
    }
```

The type `Key` is a union typed defined as (complete code [here](https://github.com/leojpod/elm-keyboard-shortcut/blob/e0f3a1ecc20e1ff5ca893a9bcebc01a2b3f103ee/src/Shortcut.elm#L70-L88)):

```elm
type Key
    = Escape
    | BackSpace
    -- | ... and many other constructors for the special keys
    | Regular String
```

### Defining a custom element

Before actually writing our custom element(s) one thing we should probably do is install a polyfill. 
While custom elements are rather well supported (see [Can I use?](https://caniuse.com/custom-elementsv1), even the Android browser joined the party!), it's still safer and nice to people who are stuck on IE11 to use a polyfill and make sure they are not left out.
There is one right [here](https://github.com/webcomponents/polyfills/tree/master/packages/custom-elements) and all you need is just to install it via NPM, ain't that simple?

Once that's done you can start by making a file for your custom element and put the following scaffolding in it.

```javascript
import '@webcomponents/custom-elements' // that's our polyfill

// custom elements are really just a custom HTMLElement
// so it is really no surprise that you just need to extends the HTMLElement class
export class ShortcutElement extends HTMLElement { 
  connectedCallback () {
    // here goes the code you want to run when your custom element is rendered and initialised
  }

  disconnectedCallback () {
    // here goes the actions you should do when it's time to destroy/remove your custom element
  }
}

// the last important step here: registering our element so people can actually use it in their HTML
customElements.define('shortcut-element', ShortcutElement)
```

If we look at the code above, the key really is in creating a new class to backup our element that extends `HTMLElement` and registering it to a tag name via `customElements.define(tagName: string, constructor: HTMLElement)`.

Now let's fill that up.
As mentioned in the comments on the snippet above, the first entry and exit points are the 2 callbacks: `connectedCallback` and `disconnectedCallback`.
The first one is called when your element is added to the page, the second one when it's taken away.

In our shortcut example, we'll use the `connectedCallback` to register an event listener on the `body` (since that will capture events regardless of what's on the page) and `disconnectedCallback` to unsubscribe our event listener from the `body`.
So we'll start with something like: 

```javascript
export class ShortcutElement extends HTMLElement {
  connectedCallback () {
    this.listener = (evt) => {
      const event = evt
      // TODO check with the associated shortcuts if we have a match
      // TODO if we have one then send a custom event
    }
    // let's register
    // NOTE: we will register at the capture phase so as to take precedence over the rest (e.g. textarea, input, ...)
    document.body.addEventListener('keydown', this.listener, { capture: true })
  }

  disconnectedCallback () {
    // let's unregister
    document.body.removeEventListener('keydown', this.listener, {
      capture: true
    })
  }
}
```

And we're almost done for the JavaScript part! Yes there are 2 big `TODO` in there but we'll get back to them after we take a look at the elm side of things


### How to use this in Elm?

On the elm side, things are rather simple. We need but 2 things: define a custom `Html.Html msg` that uses our element and find a way to communicate with that element.

The first part is super easy: `Html.node "shortcut-element"`.
To make it nice we can wrap that in a function:

```elm
shortcutElement: List (Html.Attribute msg) -> List (Html msg) -> Html msg
shortcutElement =
  Html.node "shortcut-element"
```

Now, the communication part. Well, this one has 2 subparts actually: information going to the custom element and information coming from the custom element.
For sending information from the JavaScript to Elm we'll use [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent) on the JavaScript part which means we can just use our normal [`Html.Events.on`](https://package.elm-lang.org/packages/elm/html/latest/Html-Events#on) function and the familiar [`Json.Decode`](https://package.elm-lang.org/packages/elm/json/latest/Json-Decode) (and [`Json.Decode.Extra`](https://package.elm-lang.org/packages/elm-community/json-extra/latest/Json-Decode-Extra))
For sending information down to the JavaScript from the Elm world we'll play with attributes and properties.

So it's gonna look like this: 

```elm
encodeShortcut : Shortcut msg -> Json.Encode.Value
encodeShortcut ({ keyCombination } as shortcut) =
    Json.Encode.object
        [ ( "name", Json.Encode.string <| hashShortcut shortcut )
        , ( "baseKey", Json.Encode.string <| keyToString keyCombination.baseKey )
        , ( "alt", Json.Encode.Extra.maybe Json.Encode.bool keyCombination.alt )
        , ( "shift", Json.Encode.Extra.maybe Json.Encode.bool keyCombination.shift )
        , ( "ctrl", Json.Encode.Extra.maybe Json.Encode.bool keyCombination.ctrl )
        , ( "meta", Json.Encode.Extra.maybe Json.Encode.bool keyCombination.meta )
        ]


onShortcut : List (Shortcut msg) -> Html.Attribute msg
onShortcut shortcuts =
    Html.Events.on "shortcut"
        (Json.Decode.at [ "detail", "name" ] Json.Decode.string
            |> Json.Decode.andThen
                (\hash ->
                    List.Extra.find (hashShortcut >> (==) hash) shortcuts
                        -- NOTE: if a event decoding failed then no message is emitted
                        |> Maybe.Extra.unwrap (Json.Decode.fail "did not match a known shortcut") (.msg >> Json.Decode.succeed)
                )
        )


shortcutElement : List (Shortcut msg) -> List (Html.Attribute msg) -> List (Html msg) -> Html msg
shortcutElement shortcuts attrs =
    node "shortcut-element"
        -- Add 2 attributes here: one to send the props we're listening to
        (Html.Attributes.property "shortcuts" (Json.Encode.list encodeShortcut shortcuts)
            -- one to listen to the stuff
            :: onShortcut shortcuts
            :: attrs
        )
```

(For those curious about the note on the `onShortcut` function, have a look at this [article](https://thoughtbot.com/blog/debugging-dom-event-handlers-in-elm))

The main thing here is that we're setting a property called `shortcuts` on our custom elements that contains all the shortcuts passed to the `shortcutElement` function and that we will listen to the `shortcut` event from which we are going to extract the name of our shortcut and find out which message should be sent.

In the end, the elm-side looks rather simple doesn't it?

### Huston, JavaScript speaking do you copy?

Getting back to our 2 `TODO` in JavaScript:

- find out if we have a match among the shortcut the element should listen for
- send an event if there is one.

Since the elm part will set the `shortcuts` property we can simply access this array via `this.shortcuts` from within our `ShortcutElement` class. Then one small caveat with shortcuts is the need to detect which key was really pressed since if we ask the user to press <kbd>Shift</kbd><kbd>Alt</kbd><kbd>o</kbd> for instance, the value of `event.key` might vary a lot based on the user's input method and OS (e.g. `o`, `Ø`, ...).
As explained on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent), using `event.code` would work if we assume our user are all using QWERTY keyboards but that is kind of a rubbish solution.
Instead, I'd recommend using [`deburr` from lodash](https://lodash.com/docs/4.17.15#deburr), which will remove all the "diacritical marks" (a.k.a. give you back the original letter that was pressed).

Sending out the event is as simple as using the constructor for a `CustomEvent` and setting a property in the `detail` part of its second parameter.
Putting it all together we get:

```javascript
    this.listener = (evt) => {
      const event = evt
      this.shortcuts
        .filter(
          ({ baseKey, alt, shift, ctrl, meta }) =>
            deburr(event.key).toLowerCase() === baseKey.toLowerCase() &&
            (alt == null || alt === event.altKey) &&
            (shift == null || shift === event.shiftKey) &&
            (ctrl == null || ctrl === event.ctrlKey) &&
            (meta == null || meta === event.metaKey)
        ) // now we have all the shortcuts that match the current event
        .map(({ name }) => {
          event.preventDefault()
          event.stopPropagation()
          this.dispatchEvent(
            new CustomEvent('shortcut', {
              bubbles: false,
              detail: {
                name,
                event
              }
            })
          )
        })
    }
```

To see it in action you can have a look at the [Github page here](https://leojpod.github.io/elm-keyboard-shortcut/)


## Apex Charts in Elm

[Apex charts](http://apexcharts.com/) is a fancy charting library for JavaScript that provides a lot of interactive chart types and interesting ways to combine them.
As I was looking for such library in Elm but could not quite find the one I was looking for, I thought I would make a custom element to integrate Apex charts and Elm.

In the end, it allows the dev to write things like: 

```elm
Apex.chart
    |> Apex.addLineSeries "Connections by week" (connectionsByWeek logins)
    |> Apex.addColumnSeries "Connections within office hour for that week" (dayTimeConnectionByWeek logins)
    |> Apex.addColumnSeries "Connections outside office hour for that week" (outsideOfficeHourConnectionByWeek logins)
    |> Apex.withXAxisType Apex.DateTime
```

and get a nice chart with one line and 2 columns. 

Since this post is already quite lengthy, I'll keep the second custom element for another time but you can already have a primeur of it [here](https://leojpod.github.io/elm-apex-charts-link/) (with the code [here](https://github.com/leojpod/elm-apex-charts-link/)).
To make it work, we will need to take a closer look at `getter` and `setter` in JavaScript so as to handle properties that can change over time (i.e. during the life-time of our custom element).