Surveying for interest in easier, type-safe custom elements tooling

Hi everyone,

Custom elements in Elm offer a relatively safe way to utilize JavaScript libraries that expect to be able to manipulate some section of the DOM. But hooking them up to your Elm app can be finicky, requires a lot of annoying decoding/encoding work, and there aren’t that many good learning resources out there for how to do so.

In my work in the last few months, I’ve developed a small framework for managing custom elements in Elm and TypeScript, which includes type-safe interop between Elm and TS and a headless Elm architecture abstraction in TypeScript for each custom element. It has dramatically reduced the overhead for me of managing the interop between my custom elements in TS and their use and representation in Elm. I’ve used it for integrating ProseMirror, CodeMirror, Floating UI, and Viselect in my Elm app.

I don’t have it here to present to you today as a library—rather, I was hoping to get some sense of how useful other people think it might be so that I can gauge whether I ought to extract it from my app to share. So my questions for you are:

  1. Does this sound like it would be useful to you?
  2. If you use custom elements right now in your Elm app, how do you find the experience? What are the biggest pain points you’ve experienced?

I’m also happy to answer any questions about how this works and what problems I see it as solving.

Thank you!

10 Likes

I’m very much interested in this and think it could be useful. We use a fair amount of web components at work and I use them frequently in my hobby Elm projects and making that process easier would be great.

  1. Yes, it sounds useful
  2. My biggest pain points right now wiring everything up and making sure I didn’t miss something
5 Likes

This sounds great. I still think there is a need for better Elm specific documentation around custom elements (I wrote a more specific list here).

I’m curious what exactly your tooling offers? Generally in my experience the custom elements work is mostly twofold:

  1. Doing a lot of minute work with DOM apis to get the java/type-script side to its job.
  2. Doing fancy elm API design to put enough lipstick on the pig :slight_smile:

The actual communication between these two modules tends to be a small minority of the code involved.

1 Like

The actual communication between these two modules tends to be a small minority of the code involved.

That’s interesting to hear; my experience has been different! For my use of ProseMirror and CodeMirror, especially, there are lots of messages, with payloads, that need to be sent back and forth (and therefore decoded and encoded) between the Elm and the TS, which I’ve found to be a lot of work. What use cases have you run into for custom elements? Maybe they’ve involved less data interchange?

I’m curious what exactly your tooling offers?

This tool would offer type-safe interop between the custom elements and Elm, and scaffolding for connecting the Elm representation of a custom element with the JS (TS, in our case) representation of it.

I did some API sketching today to better illustrate how it would work, which I’ll share below. This is a better and more generalized version of what I have right now, but is what I imagine extracting it into a library might look like.


API Sketch

Library code

CustomElement.elm
module CustomElement exposing (..)

type Model model

type Msg msg

define :
    { name : String
    , flagsCodec : Codec flags
    , toTsMsgCodec : Codec toTsMsg
    , toElmMsgCodec : Codec toElmMsg
    )
    -> { init : flags -> model
       , update : (toTsMsg -> Cmd msg) -> ( toElmMsg, model ) -> ( model, Cmd toElmMsg )
       }
    -> ( Definition flags toTsMsg toElmMsg model
       , { init : flags -> Model model
         , update : ( Msg toElmMsg, Model model ) -> ( Model model, Cmd (Msg toElmMsg) )
         , view : Model model -> Html (Msg toElmMsg)
         , send : toTsMsg -> Model model -> Cmd (Msg toElmMsg)
         }
       )
custom-element.ts
export type Definition<Flags, ToTsMsg, ToElmMsg>

export type Registration

export const register: <Flags, ToTsMsg, ToElmMsg, Model>(
  definition: Definition<Flags, ToTsMsg, ToElmMsg>,
  init:
    (send: (toElmMsg: ToElmMsg) => void)
    => (htmlElement: HTMLElement)
    => (flags: Flags)
    => Model,
  update:
    (send: (toElmMsg: ToElmMsg) => void)
    => (htmlElement: HTMLElement)
    => (toTsMsg: ToTsMsg, model: Model)
    => Model,
) => Registration

export const init: (registrations: Registration[], elmApp: unknown) => void

App code

Generated

generated/CustomElementPorts.elm
-- Contains the generated ports for interop.
generated/custom-element-definitions.d.ts
// Contains the types for the custom element definitions defined in the Elm.

Usage

src/elm/CustomElementDefinitions.elm
-- This is where you expose all of your custom element definitions
-- to the library CLI to parse and generate TS types from.

module CustomElementDefinitions exposing (customElementDefinitions)

import CustomElement
import RichTextEditor



customElementDefinitions : List CustomElement.Definition
customElementDefinitions =
    [ RichTextEditor.customElementDefinition ]
src/ts/custom-elements/rich-text-editor.ts
import type { Registration } from "custom-elements";
import type { richTextEditor } from "generated/custom-element-definitions"


type Model

const init:
  (send: (toElmMsg: richTextEditor.ToElmMsg) => void)
  => (htmlElement: HTMLElement)
  => (flags: richTextEditor.Flags)
  => Model

const update:
  (send: (toElmMsg: richTextEditor.ToElmMsg) => void)
  => (htmlElement: HTMLElement)
  => (toTsMsg: richTextEditor.ToTsMsg, model: Model)
  => Model

export const customElementRegistration: Registration
src/index.ts
import customElements from "custom-elements";
import richTextEditor from "src/ts/custom-elements/rich-text-editor";
import { Elm } from "src/elm/Main";


const customElementRegistrations = [richTextEditor.customElementRegistration]
const elmApp = Elm.Main.init()

customElements.init(customElementRegistrations, elmApp)
1 Like