TLDR: We all know that there is an easy way to embed Elm into other frameworks and existing app. What are the ways of embedding other libs or frameworks into Elm app?
We are rewriting our Ember app to Elm by embedding parts of Elm into existing Ember app. We are close to the point, where we will have majority of our codebase in Elm but we are unable to switch to the Elm SPA as we are having some complex and critical parts in Ember. So we still have to have Ember shell around our Elm app until we will rewrite all features of Ember app into Elm. And that sucks.
I would like to open discussion around possibility of embedding other libs into the Elm as smooth as Elm is embeddable into existing codebase.
We have quite a good experience with x-tag. We are having Web Component specified as x-tag and from Elm we are creating that tag using Html.node passing the model as attributes. We are not in need to react to things inside the XTag so far (but I think that problem will arise and it will can be solved by ports back into Elm).
Do you have another idea/experience? Do you think it is a good idea to embed components from other libraries into Elm in the first place or it is a blasphemy and non-sense?
Using a Custom Element is the best way to do this (if your users’ browsers support them), so you’re already on your way. When it comes time to communicate between your Ember components and your Elm program you can:
Accept information from Elm using a getter and setter for a property. You’ve already gotten this working.
Notify Elm of changes by triggering a custom event on your element
I found the x-tag documentation difficult to follow, but I can share how to do it using the Custom Elements v1 API and your experience with x-tag will let you translate. The example is probably going to retread what you’ve already done to send stuff into the Ember program, but the interesting thing I think is triggering events from the custom element to notify Elm of changes.
JavaScript:
customElements.define('ember-program', class FancyButton extends HTMLElement {
constructor() {
super()
this._emberApp = null
this._model = null
}
get model() {
return this._model
}
set model(value) {
this._model = value
if (!this._emberApp) return
// however you actually send a value to an ember program
this._emberApp.setModel(value)
}
connectedCallback() {
// however you actually start an ember program
// where `this` is the root element and `this._model` is the initial state
this._emberApp = startEmberApp(this, this._model)
// notify the Elm program when ever something interesting happens in your
// app using whatever notification system Ember provides
this._emberApp.on('changed', () => {
this.dispatchEvent(new CustomEvent('changed'))
})
}
disconnectedCallback() {
this._emberApp.destroy() // or whatever it actually is
}
})
Elm:
module EmberApp exposing (view, Model)
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode exposing (Value)
import Html exposing (Html)
import Html.Attributes as Attributes
import Html.Events as Events
type alias Model =
{ whatever : String }
encoder : Model -> Value
encoder model =
Encode.object [ ("whatever", Encode.string model.whatever ) ]
decoder : Decoder Model
decoder =
Decode.map Model
(Decode.field "whatever" Decode.string)
view : (Model -> msg) -> Model -> Html msg
view onChange model =
Html.node "ember-program"
[ Attributes.property "model" <| encoder model
-- in the decoder, `target` is the element instance and `model` is the
-- getter for the model that we set up in JS
, Events.on "changed" <|
Decode.map onChange (Decode.at [ "target", "model" ] decoder)
]
[]
The most common pushback I see about this technique is that people expect that the encoding of the model on every render should be slow if your model is really big. There is no reason this should be true by default! It needs to be measured. If profiling shows that encoding is your bottleneck, then some optimizations you can explore are:
Use Html.Lazy if your Ember app’s model is not constrructed inline, and can live in your Elm program’s model as-is
Track changes to the model with a counter and use that counter as the key in Html.Keyed
If you’ve gotten a lot done with x-tag you probably don’t need to! x-tag is just an API for making custom elements. All I meant to say about it is that I couldn’t figure out at-a-glance how to do the things I wanted to show with x-tag’s API.
When using custom elements with state, you want to be very aware of how the virtual DOM diff logic works because lifecycle can be critical for those elements and having the virtual DOM introduce either a spurious destroy/re-create for what you thought of as a single element or a reuse for what you thought of as two elements can be a source of subtle and sometimes difficult to reproduce bugs.
Using Html.Keyed around your custom element with appropriate keys can fix the accidental reuse problem.
The accidental destroy/re-create issue takes more work because you need to make sure that the DOM diff code matches things up the way you do. If you can make the indexed path (e.g., second element then third element then first element) stable this will solve that problem. You can use text “” as a null element for optional parts of the tree. If that won’t work, Html.Keyed can help but beware that it only does a one element look ahead and there are no guarantees around how often the DOM updates.
This is not to say “don’t use custom elements”. It’s just to say beware of the issues you may need to address in your use of them to avoid weird bugs down the road. (For that matter, all of these issues also apply to textfields when they have a selection active. We just don’t generally notice them because the DOM rarely mutates in the vicinity of the textfield while it has focus.)
Mark
P.S. I remember (misremember?) one of the earlier drafts for what became Html.Keyed being based around supplying both a list of keys and a dictionary of keyed elements. That would have made this problem easier but would have been a more complicated API and would have created its own set of complexities for backend behavior if the same key were used more than once in the list.