Web Components with Ionic 4 and Elm: Promises and Pitfalls

Although no firm date has been set, the release of the next version of the Ionic framework is approaching. One of the most exciting changes is the shift to using web components built with Stencil for the "core" of the framework. This promises to free Ionic from its dependency on Angular and allow it to be used with any framework, or with none at all.

Combining the simple, mobile friendly UI elements of Ionic with the joy that is building a web app in Elm has always been a dream of mine, so I decided to give it a try using the alpha version of Ionic 4. There were a couple of hurdles, but overall it worked great! And thanks to some nice tooling the end result is a fully offline capable Progressive Web App, no extra configuration required.

First I'll go over the process of getting the example app up and running, and then discuss some of the potential challenges in using this approach to build a production application.

If you'd like to see the app in action, you can see the final version on github pages (and add it to your home screen as a PWA!), or play with the code in this Ellie sandbox.

App code

First install create-elm-app and generate a new Elm project by running

elm-app create ionic-4-elm

Modify Main.elm to match

module Main exposing (..)

import Html exposing (Html, div, h1, img, node, text)
import Html.Events exposing (onClick)


---- MODEL ----


type alias Model =
    { counter : Int
    }


init : ( Model, Cmd Msg )
init =
    ( { counter = 0 }, Cmd.none )



---- UPDATE ----


type Msg
    = NoOp
    | Increment
    | Decrement


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        NoOp ->
            model ! []

        Increment ->
            { model | counter = model.counter + 1 } ! []

        Decrement ->
            { model | counter = model.counter - 1 } ! []

---- VIEW ----

view : Model -> Html Msg
view model =
    div []
        [ Html.p [] [ text "Elm is here!" ]
        , node "ion-button" [ onClick Increment ] [ text "+" ]
        , node "ion-button" [ onClick Decrement ] [ text "-" ]
        , Html.p [] [ text <| "Count is " ++ toString model.counter ]
        ]


main : Program Never Model Msg
main =
    Html.program
        { view = view
        , init = init
        , update = update
        , subscriptions = always Sub.none
        }

Most of this is standard reactive app counter demo boilerplate, but its worth pointing out the use of ion-button in the view function; as far as Elm is concerned we're just using a plain old HTML tag. Any rich behavior is hidden inside the component, and the Elm side (notionally) doesn't have to worry about it.

Finally, modify public/index.html to match

<!DOCTYPE html>
<html lang="en">
  <head>
      <meta charset="utf-8">
      <meta http-equiv="x-ua-compatible" content="ie=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1, user-scalable=0">
      <meta name="theme-color" content="#000000">

      <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
      <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
      <script src="https://unpkg.com/@ionic/core@4.0.0-alpha.7/dist/ionic.js"></script>
      <title>Elm App</title>
  </head>
  <body>
    <ion-app>
      <ion-header>
        <ion-toolbar color='primary'>
          <ion-title>Elmonic</ion-title>
        </ion-toolbar>
      </ion-header>
      <ion-content padding>
        <div id="root"></div>
      </ion-content>
    </ion-app>
  </body>
</html>

Don't forget the script tag pulling in Ionic Core from Unpkg! Eventually you'd want to move this into the build pipeline, but for now loading Ionic from a CDN will get the demo up and running without having to change any Webpack configurations.

At this point you can see the app in action by running elm-app start from the project root and browsing to localhost:3000.

You may be wondering why so many components are in the HTML template and not in the Elm code. Unfortunately the answer is "it won't work otherwise". If Elm is responsible for rendering everything from ion-app on down, any changes to the state (in this case clicking the plus or minus buttons) cause the Elm framework to throw a Cannot read property 'replaceData' of undefined runtime error.

I'll come back to this issue later on, but for now embedding the Elm app inside of the ion-content element is a good enough workaround for a proof of concept.

And there we have it. Not the most exciting web app of course, but I was impressed at how closely Ionic's web components already come to the ideal "it just works" experience. Even better, since the project was generated with create-elm-app all you need to do is make a few configuration changes and you're ready to deploy a full PWA to Github Pages or another static hosting provider.

Enthusiasm Dampers

I'm pretty happy with how easy it was to start using web components in Elm, and about the prospects for using it to build PWAs and hybrid mobile apps. But a couple of issues give me pause when I think about using this approach in a larger production setting.

First there're the usual concerns around interacting with the outside world from Elm; some of the nice runtime safety guarantees are lost (or at least become more complicated), and working with any state managed by a component is likely to involve a lot of messing around with ports. Still, these aren't exactly new concerns in the Elm ecosystem, and projects like Ossi Hanhinen's elm-ement point toward the possibility of managing them with some well designed abstractions.

But then there's the runtime error that happens when trying to render the whole view from Elm. Its not a small problem, since many of the more interesting and complex Ionic components (eg the navigation layer) operate at a higher level than the ion-content tag. I talked to some folks in the Ionic Slack, and did some digging around in the Elm internals, but all I can really conclude with my limited Elm and Ionic knowledge is that Elm is losing its reference to a part of the DOM it expects to update as the result of application state changes.

It's possible there's a better workaround, or that the problem will go away as Ionic 4 develops further (it is still in alpha after all). But I worry this could be a common issue when trying to use web components with a framework that exercises a lot of control over the DOM. If Elm is trying to render a web component that is in turn expecting to control the rendering of its own child elements, aren't we bound to run into trouble? If so, is this a retreat from the web components elevator pitch of being able to freely use fully encapsulated components in any HTML context? Even if this particular problem is just a quirk of Elm's implementation (I was unable to reproduce the issue in a similarly structure React app), its exactly the sort of abstraction-boundary-crossing concern that makes me want to stay in the walled garden of any given front end framework.

Still, I'm hopeful. The momentum behind web components is clearly there, and I expect some of these edge cases to get ironed out as the various supporting standards and technologies develop further. Its still early days for web components and already its possible to build a sleek looking, mobile friendly PWA using "off the shelf" components, even in a niche framework like Elm.