Skip to content

Instantly share code, notes, and snippets.

@Janiczek
Last active October 12, 2021 10:30
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Janiczek/06134ccb47a916990e518c48eaa1a0da to your computer and use it in GitHub Desktop.
Save Janiczek/06134ccb47a916990e518c48eaa1a0da to your computer and use it in GitHub Desktop.
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 });
}
}
};
};
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 ++ "!")
import { elmWebComponent } from '../../../helpers/elm-web-component';
import { Elm } from './ElmHelloWorld.elm';
export const ElmHelloWorld = elmWebComponent(Elm.ElmHelloWorld, {
observedProps: ['name'],
optionalProps: ['name']
});
import { ElmHelloWorld } from './ElmHelloWorld';
customElements.define('elm-hello', ElmHelloWorld);
// you can now create <elm-hello name="World" />
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