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)