Extending Elm Native UI with JavaScript

Jon Yurek

You may have heard about our foray into writing a native app using Elm Native UI. Purple Train accomplishes its goals quite well for us. It’s not only a useful product, it’s also a learning experience for us. We decided that it’s only fair for us to try to push the boundaries of the technology.

After we submitted the first Elm version to the Apple App Store, I entered what I thought would be a simple issue on the project: when you open the Stop Picker, it should be scrolled to the stop that’s selected instead of being at the top of the list. It became a prime example of why you should not tell a developer that something is “just a small change”.

Imperativeness in a land of Declarations

The biggest problem is that when the Stop Picker opens, we want code to execute that affects the state of the UI. The Elm Architecture builds its UIs in a declarative manner. You say “There’s a scroll view here and it contains a list of buttons” and the display layer takes care of laying that out on the screen for you. There’s no place to say “When the box is displayed, execute this code”.

In order to get this to work, we decided to play nice with declarations instead of trying to fight them. We wanted a way to declare that a given element should be marked as the scroll target when the box is rendered. You’ve undoubtedly seen this pattern when using a loop to generate some HTML <option> tags and one is <option selected="true">. We had to make the function that generates the button be able to tell if it’s the selected one. We also had to be able to pass the selected button down the chain so the function can figure that out.

The code to generate a button in the Stop Picker originally looked like this:

stopButton : Stop -> Node Msg
stopButton stop =
    pickerButton (PickStop stop) stop

Where pickerButton called Element.touchableHighlight to create the button and PickStop stop is the message we send our event loop.

We changed the stopButton function to this:

stopButton : Maybe Stop -> Stop -> Node Msg
stopButton highlightStop stop =
    case highlightStop of
        Nothing ->
            pickerButton (PickStop stop) stop

        Just pickedStop ->
            if stop == pickedStop then
                highlightPickerButton (PickStop stop) stop
            else
                pickerButton (PickStop stop) stop

And we pass in the already-selected stop or Nothing if there isn’t one.

The main difference between pickerButton and highlightPickerButton is that they pass a scrollTarget property to the touchableHighlight call. This lets the underlying component know where it should scroll to on load.

This whole setup lets us define what’s special about the situation using Elm instead of using JS. We tried a few ways of letting Elm handle React Native’s componentDidMount callback, but ultimately had to fall back to JavaScript.

The State of the UI

Since Elm Native UI is built on top of React Native, it’s possible to drop down into that more imperative land of JavaScript to do things we can’t do (or, at least, we haven’t yet specified a way to do) in Elm. Thankfully, since Elm compiles to JavaScript, it’s rather straightforward to craft some JavaScript code that Elm is able to use.

What we need here is a React component that knows that it needs to find and scroll to a sub-element once it’s mounted into the UI. We’ll need to hook into the componentDidMount callback because we need values (height, specifically) from the rendered state of the component.

In the file app/Native/ScrollWrapper.js, we created a component subclass that looks like this:

const _user$project$Native_ScrollWrapper = function () {
  var ScrollView = require('ScrollView');
  class ScrollWrapper extends ScrollView {
    componentDidMount() {
      // Find the child node that has the `scrollTarget` property
      // Scroll to that node with `this.scrollTo();`
    }
  }

  return {
    view: ScrollWrapper
  };
}();

Because we named the const_user$project$Native_ScrollWrapper”, we’ll be accessing it in Elm as Native.ScrollWrapper. We also need this Elm code in app/ScrollWrapper.elm:

module ScrollWrapper exposing (view)

import NativeUi exposing (Node, Property)
import Native.ScrollWrapper


view : List (Property msg) -> List (Node msg) -> Node msg
view =
    NativeUi.customNode "ScrollWrapper" Native.ScrollWrapper.view

Once we have this in place, we can import ScrollWrapper in our Elm view and we can replace the call to Element.scrollView with ScrollWrapper.view.

Now that all this is in place, we can refresh the app in the iOS Simulator and see the results:

Autoscroll Demo

Getting Things Done

I admit, this isn’t the best thing to have to do. I’d much prefer that we can specify this kind of extension in pure Elm. That’s the point of writing Elm, after all. But it’s very useful that we can interact with JavaScript in this way. Even if we could write new functionality to extend the framework in Elm, there is a lot of existing code written in JavaScript that could be very useful to include in a project. It’s comforting to know that it’s fairly easy to access should we need it.