5 min read

jQuery as a library has proven itself as an invaluable asset in the process of development by leveraging the complexity of the DOM API from web programmer’s shoulders. For a very long time, jQuery Plugins were one of the best ways of writing modular and reusable code.

In this post, I will explain how to integrate existing jQuery plugin into your Elm application. At first glance, this might be a bad idea, but bear with me; I will show you how you can benefit from this combination. The example relies on Create Elm App to simplify the setup for minimal implementation of the jQuery plugin integration.

As Richard Feldman mentions in his talk from ReactiveConf 2016, you could, and actually should, take over the opportunity of integrating existing JavaScript assets into your Elm application.

As an example, I will use Select2 library, which I have been using for a very long time. It is very useful for numerous use cases as native HTML5 implementation of <select> element is quite limited.

The view

You are expected to have some experience with Elm and understand application life cycle.

The first essential rule to follow is that when establishing interoperation with any User Interface, components should be written in JavaScript. Do not ever mutate the DOM tree, which was produced by the Elm application. Introducing mutations to elements, defined in the view function, will most likely introduce a runtime error. As long as you follow this simple rule, you can enjoy the best parts of both worlds, having complex DOM interactions in jQuery, and strict state management in Elm.

Hosting a DOM node for a root element of a jQuery plugin is safe, as long as you don’t use the Navigation package for client-side routing. In that case, you will have to define additional logic for detaching the plugin and initializing it back when the route changes.

I’ll stick to the minimal example and just define a root node for a future Select2 container.

view : Model -> Html Msg
view model =
   div
       []
       [ text (toString model)
       , div [ id "select2-container" ] []
       ]

You should be familiar with the concept of JavaScript Interop with Elm; we will use both the incoming and outgoing port to establish the communication with Select2.

Modeling the problem

The JavaScript world gives you way more options on how you might wire the integration, but don’t be distracted by the opportunities because there is an easy solution already. The second rule of robust interop is keeping the state management to Elm. The application should own the data for initializing the plugin and control its behavior to make it predictable. That way, you will have an extra level of security, almost with no effort.

I have defined the options for the future Select2 UI element in my app using a Dictionary. This data structure is very convenient for rendering <select> node with <options> inside.

options : Dict String String
options =
   Dict.fromList
       [ ( "US", "United States" )
       , ( "UK", "United Kingdom" )
       , ( "UY", "Uruguay" )
       , ( "UZ", "Uzbekistan" )
       ]

Upon initialization, the Elm app will send the data for future options through the port.

port output : List ( String, String ) -> Cmd msg

Let’s take a quick look at the initialization process here as you might have a slightly different setup; however, the core idea will always follow the same routine. Include all of the libraries and application, and embed it into the DOM node.

// Import jQuery and Select2.
var $ = require('jquery');
var select2 = require('select2');

// Import Elm application and initialize it.
var Elm = require('./Main.elm');
var root = document.getElementById('root');
var App = Elm.Main.embed(root);

When this code is executed, your app is ready to send the data to outgoing ports.

Port communication

You can use the initial state for sending data to JavaScript right after the startup, which is probably the best time for initializing all user interface components.

init : ( Model, Cmd Msg )
init =
   ( Model Nothing, output (Dict.toList options) )

To retrieve this data, you have to subscribe to the port in the JavaScript land and define some initialization logic for a jQuery plugin.

When the output port will receive the data for rendering options, the application is ready for initialization of the plugin. The only implication is that we have to render the <select> element with jQuery or any other templating library.

When all the required DOM nodes are rendered, the plugin can be initialized. I will use the change event on Select2 instance and notify the Elm application through the input port. To simplify the setup, we can trigger the change event right away so the state of the jQuery plugin instance and Elm application is synchronized.

App.ports.output.subscribe(function (options) {

   var $selectContainer = $('#select2-container');

   // Generate DOM tree with <select> and <option> inside and embed it into the root node.
   var select = $('<select>', {
       html: options.map(function (option) {
           return $('<option>', {
               value: option[ 0 ],
               text: option[ 1 ]
           })
       })
   }).appendTo($selectContainer);

   // Initialize Select2, when everything is ready.
   var select2 = $(select).select2();

   // Setup change port subscription.
   select2.on('change', function (event) {
       App.ports.input.send(event.target.value);
   });

   // Trigger the change for initial value.
   select2.trigger('change');
});

The JavaScript part is simplified as much as possible to demonstrate the main idea behind subscribing to outgoing port and sending data through an incoming port.

You can have a look at the running Elm application with Select2.

The source code is available at GitHub in halfzebra/elm-examples.

Conclusion

Elm can get along with pretty much any JavaScript framework, or a library if said framework or library is not mutating the DOM state of Elm application. The Elm Architecture will help you make Vanilla JavaScript components more reliable.

Even though ports cannot transport any abstract data types, except built-in primitives, which is slightly limiting, Elm has high interop potential with most of the existing JavaScript technology.

If done right, you can get the extreme diversity of functionality from a huge JavaScript community under the control of the most reliable state machine on the frontend!

About the author

Eduard Kyvenko is a frontend lead at Dinero. He has been working with Elm for over half a year and has built a tax return and financial statements app for Dinero. You can find him on GitHub at @halfzebra.

LEAVE A REPLY

Please enter your comment!
Please enter your name here