Phoenix, Elm, and multiple single-page apps

In the running example used in Programming Phoenix, client-side apps are launched from the master app.js file. The Javascript that's loaded for every page checks to see whether this page has the id it's looking for. If so, the code to launch the app is run.

That's fine for the book, as it avoids irrelevant complexity, but I found it awkward as my overall app grew. Multiple clauses of this form:

const ivDiv = document.querySelector('#iv-target');  
if (ivDiv) {  
    Elm.IV.embed(ivDiv);
}

... are untidy. I'd rather have the "start the client-side" code exist only on the single page for the app. For this Javascript newbie, that turned out to be harder than I'd expected. So I've frozen a branch that shows how, and I explain it below.


Disclaimers:

  • This is for Phoenix 1.2.1 and Elm 0.17.
  • The code is a snapshot of a fair amount of learning-through-flailing about Elm, Phoenix, and the Bulma CSS library. The master branch may be a better guide going forward. This version might be easier to understand, though - less generalization.
  • Probably I'm doing things more awkwardly than need be. I may update this post if people tell me how.

About the Elm code

  • The Elm code won't take over the whole screen. Instead, it will be embedded inside a standard header and footer. (Some pages of the overall app are just boring old HTML.)

  • Perhaps mistakenly, I'm dividing the overall app into multiple single-page apps. The overall app will manage teaching animals for schools of Veterinary medicine. So there will be a single-page app for managing animals, one for managing medical procedures and the business rules around them, one for making or modifying reservations, and so on. Here, we'll be dealing with the very beginnings of the animal-management part. (Elm Source. Right now, it just puts a little information in a <p> tag:

        view : Model -> Html Msg
        view model =
          p []
            [text <| "Animal has been started with argument \""
               ++ model.aStringFlag ++ "\" and "
               ++ toString(model.aNumberFlag)]
  • The app uses Html.App.programWithFlags. Since I found passing arguments to Elm puzzling at first, I figure the example might be useful. However, I won't be explaining it here.

About Brunch and the layout file

My brunch-config.js file is modified from one by OvermindDL1. The key clause is this:

    elmBrunch: {
      elmFolder: "web/elm",
      mainModules: ["IV.elm", "Animals.elm"],
      outputFolder: "../static/js",
      outputFile: "critter4us.js"
    },

The important bit here is that all the various Elm apps (plus the Elm runtime) are bundled together into a single critter4us.js file.

After that's done, there's a further step that bundles critter4us.js with other Javascript files into a single app.js file that gets dumped in priv/static/js/app.js. Phoenix's standard app.html.eex layout loads that on every page:

<!DOCTYPE html>  
<html lang="en">  
  <head>...</head>

  <body>
    ...
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

Because of historical accident, I don't use app.html.eex, but my layout file follows the same pattern.

The question at hand is: what page-specific code to put after that <script src= line in order to launch the single-page app appropriately?

The Javascript to start the Elm app

The answer is this:

<script type="text/javascript">  
  const elmDiv = document.querySelector('#embed-elm-here');
  require("web/static/js/critter4us.js").Animals.embed(elmDiv, {
     aNumberFlag: "5845",  aStringFlag: "string", 
  })
</script>  

Notice that:

  1. The require refers to the name of an intermediate file. That is, we have to refer to the result of the Brunch step that produces web/static/js/critter4us.js. I was surprised I had to refer to it, given that it's neither a source file nor the final result (priv/static/js/app.js). But that's the way it is.

  2. Note the dereference after require(...) is Animals. That matches the definition of my single-page app: module Animals exposing (main). I'd like to think it would all work if I were trying to launch MyBigApp.Animals, but I haven't tried it.

  3. (An aside) As far as I can tell, you can only pass strings to an Elm programWithFlags. So the Elixir side has to convert the integer 5845 into a string, which the Elm side must convert back into an integer - even though Javascript would be fine handling both strings and integers.

Various files

  • You wouldn't want a reference to a specific Elm app to be in this file - the layout used for all pages. At the moment, I use this rather crunky code:
  <%= if Map.has_key?(assigns, :elm_launcher), do: @elm_launcher, else: "" %>
  • For the moment, the Javascript shown above is generated in the controller.

  • I wanted each controller action to use a <div id="embed-elm-here"> while also wrapping that div in a layout. I couldn't quickly find a way to do that, so I made a dumb template that contained nothing but that div. In the controller, it's rendered with render(Eecrit.ElmView, "elm_hook.html"). That means controllers look like this:

  def index(conn, _params) do
    conn
    |> launch_page_app_with_flags(aStringFlag: "string", aNumberFlag: 5845)
    |> render(Eecrit.ElmView, "elm_hook.html")
  end