Created
February 24, 2022 22:32
-
-
Save Janiczek/32075e1b9bbc2721db17b59150a4a473 to your computer and use it in GitHub Desktop.
Elm WebComponents
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { LitElement } from 'lit-element'; | |
// adapted from https://github.com/thread/elm-web-components (we need to not register the component eagerly) | |
// adapted from https://github.com/PixelsCommander/ReactiveElements | |
const camelize = str => | |
// adapted from https://stackoverflow.com/questions/2970525/converting-any-string-into-camel-case#2970667 | |
str | |
.toLowerCase() | |
.replace(/[-_]+/g, ' ') | |
.replace(/[^\w\s]/g, '') | |
.replace(/ (.)/g, firstChar => firstChar.toUpperCase()) | |
.replace(/ /g, ''); | |
const processJson = (name, value) => { | |
try { | |
value = JSON.parse(value); | |
} catch (e) {} | |
return value; | |
}; | |
const getProps = el => { | |
const props = {}; | |
for (let i = 0; i < el.attributes.length; i++) { | |
const attribute = el.attributes[i]; | |
const name = camelize(attribute.name); | |
props[name] = processJson(attribute.name, attribute.value); | |
} | |
return props; | |
}; | |
export const elmWebComponent = ( | |
elmComponent, | |
{ observedProps = [], setupPorts = () => {}, optionalProps = [], staticProps = {}, onDetached = () => {} } = {} | |
) => { | |
const context = {}; | |
return class ElmWebComponent extends HTMLElement { | |
static get observedAttributes() { | |
return observedProps; | |
} | |
connectedCallback() { | |
try { | |
let shadowContents = {}; | |
if (this.childNodes && this.childNodes.length > 0) { | |
const key = this.nodeName.toLowerCase() + '-content'; | |
shadowContents[key] = []; | |
for (let child of this.childNodes) { | |
shadowContents[key].push(child); | |
} | |
} | |
let props = Object.assign({}, Object.fromEntries(optionalProps.map(a => [a, null])), getProps(this), staticProps); | |
if (Object.keys(props).length === 0) { | |
props = undefined; | |
} | |
const parentDiv = this; | |
const elmDiv = document.createElement('div'); | |
parentDiv.appendChild(elmDiv); | |
const elmElement = elmComponent.init({ | |
flags: props, | |
node: elmDiv | |
}); | |
context.ports = elmElement.ports; | |
setupPorts.call(this, elmElement.ports); | |
const setCustomContent = (elementName, value) => { | |
if (value && value.length > 0) { | |
let element = elmDiv.querySelector(elementName); | |
if (element) { | |
const shadow = element.attachShadow({ mode: 'open' }); | |
for (var child of value) { | |
shadow.appendChild(child); | |
} | |
} | |
} | |
}; | |
for (let i in shadowContents) { | |
setCustomContent(i, shadowContents[i]); | |
} | |
} catch (error) { | |
console.error(error, context); | |
} | |
} | |
disconnectedCallback() { | |
onDetached(); | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
if (oldValue === newValue) { | |
return; | |
} | |
if (context.ports) { | |
newValue = processJson(name, newValue); | |
oldValue = processJson(name, oldValue); | |
context.ports.propChanged.send({ name, oldValue, newValue }); | |
} | |
} | |
}; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module ElmHelloWorld exposing (main) | |
import Html exposing (Html) | |
import Json.Decode as Decode exposing (Value) | |
import WebComponent | |
type alias Flags = | |
{ name : Maybe String } | |
type alias Model = | |
{ name : String } | |
type alias Msg = | |
Never | |
main : WebComponent.Program Flags Model Msg | |
main = | |
WebComponent.element | |
{ init = init | |
, update = update | |
, view = view | |
, subscriptions = subscriptions | |
, onPropChanged = onPropChanged | |
} | |
init : Flags -> ( Model, Cmd Msg ) | |
init flags = | |
( { name = flags.name |> Maybe.withDefault "World" } | |
, Cmd.none | |
) | |
onPropChanged : { name : String, oldValue : Value, newValue : Value } -> Model -> ( Model, Cmd Msg ) | |
onPropChanged { name, oldValue, newValue } model = | |
case name of | |
"name" -> | |
case Decode.decodeValue Decode.string newValue of | |
Ok newName -> | |
( { model | name = newName }, Cmd.none ) | |
Err _ -> | |
( model, Cmd.none ) | |
_ -> | |
( model, Cmd.none ) | |
update : Msg -> Model -> ( Model, Cmd Msg ) | |
update msg model = | |
( model, Cmd.none ) | |
subscriptions : Model -> Sub Msg | |
subscriptions model = | |
Sub.none | |
view : Model -> Html Msg | |
view { name } = | |
Html.text ("Hello, " ++ name ++ "!") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { elmWebComponent } from './elm-web-component'; | |
import { Elm } from './ElmHelloWorld.elm'; | |
export const ElmHelloWorld = elmWebComponent(Elm.ElmHelloWorld, { | |
observedProps: ['name'], | |
optionalProps: ['name'] | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
port module WebComponent exposing (Program, element) | |
import Browser | |
import Html exposing (Html) | |
import Json.Decode exposing (Value) | |
type alias Program flags model msg = | |
Platform.Program flags model (Msg msg) | |
type Msg innerMsg | |
= PropChanged | |
{ name : String | |
, oldValue : Value | |
, newValue : Value | |
} | |
| InnerMsg innerMsg | |
port propChanged : | |
({ name : String | |
, oldValue : Value | |
, newValue : Value | |
} | |
-> msg | |
) | |
-> Sub msg | |
type alias InnerConfig flags model msg = | |
{ init : flags -> ( model, Cmd msg ) | |
, update : msg -> model -> ( model, Cmd msg ) | |
, view : model -> Html msg | |
, subscriptions : model -> Sub msg | |
-- here comes the interesting part: | |
, onPropChanged : | |
{ name : String | |
, oldValue : Value | |
, newValue : Value | |
} | |
-> model | |
-> ( model, Cmd msg ) | |
} | |
element : InnerConfig flags model msg -> Platform.Program flags model (Msg msg) | |
element inner = | |
Browser.element | |
{ init = init inner | |
, view = view inner | |
, update = update inner | |
, subscriptions = subscriptions inner | |
} | |
init : InnerConfig flags model msg -> flags -> ( model, Cmd (Msg msg) ) | |
init inner flags = | |
inner.init flags | |
|> Tuple.mapSecond (Cmd.map InnerMsg) | |
view : InnerConfig flags model msg -> model -> Html (Msg msg) | |
view inner model = | |
inner.view model | |
|> Html.map InnerMsg | |
update : InnerConfig flags model msg -> Msg msg -> model -> ( model, Cmd (Msg msg) ) | |
update inner msg model = | |
(case msg of | |
PropChanged data -> | |
inner.onPropChanged data model | |
InnerMsg innerMsg -> | |
inner.update innerMsg model | |
) | |
|> Tuple.mapSecond (Cmd.map InnerMsg) | |
subscriptions : InnerConfig flags model msg -> model -> Sub (Msg msg) | |
subscriptions inner model = | |
Sub.batch | |
[ propChanged PropChanged | |
, inner.subscriptions model |> Sub.map InnerMsg | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment