How to coordinate rendering with port interoperability (Elm 0.17)

I would like to integrate Elm with the Javascript library so that Elm dynamically creates "cells" (html divs), and Javascript is provided with their identifiers and uses them to perform custom operations. The sequence I want to have is

  • Elm creates a cell (and assigns an id)
  • A message with an identifier is sent to the port
  • Javascript receives the message and performs its action

Here is how I implemented this at the beginning ( full source ):

port onCellAdded : CellID -> Cmd msg update : Msg -> Model -> (Model, Cmd Msg) update message ({cells} as model) = case message of Push -> let uid = List.length cells in ({ model | cells = [uid] ++ cells }, onCellAdded uid) 

The problem was that Javascript on the other hand

 var container = document.getElementById('app'); var demoApp = Elm.RenderDemo.embed(container); demoApp.ports.onCellAdded.subscribe(function(cellID) { if(document.getElementById('cell:' + cellID) === null) { window.alert("Cannot find cell " + cellID) } }); 

complained that such an identifier was not found. Obviously, the view has not yet been shown.

So, I added another application ( OnCellAdded ) to the Elm application, hoping the thread would be like this:

  • Elm creates a cell (in Push) and requests ( Task.perform ) asynchronous task OnCellAdded
  • Here is a view
  • OnCellAdded is called and a message with an identifier is sent to the port
  • Javascript receives the message and performs its action

The implementation looked like this ( diff ) ( full source ):

 update message ({cells} as model) = case message of Push -> let uid = List.length cells in ({ model | cells = [uid] ++ cells }, msgToCmd (OnCellAdded uid)) OnCellAdded counter -> (model, onCellAdded counter) msgToCmd : msg -> Cmd msg msgToCmd msg = Task.perform identity identity (Task.succeed msg) 

But still, OnCellAdded processed immediately after Push without intermediate rendering of the model.

My last attempt was to use Update.andThen ( diff ) ( full source )

 Push -> let uid = List.length cells in ({ model | cells = [uid] ++ cells }, Cmd.none) |> Update.andThen update (OnCellAdded uid) 

However, this does not work. I need help here.

+6
source share
3 answers

As with 0.17.1 , there is no good way to do this.

The easiest way would be to use setTimeout to wait at least 60 ms or wait for the next requestAnimationFrame

Consider the following example:

 demoApp.ports.onCellAdded.subscribe(function(cellID) { setTimeout(function() { if(document.getElementById('cell:' + cellID) === null) { window.alert("Cannot find cell " + cellID) } }, 60); }); 

There is a feature request # 19 to add a hook, so you can find out when the HTML Node is in the DOM.

You can do it here , most likely it will be in future releases.

+5
source

Implementation using requestAnimationFrame ()

Nowadays, this seems like the cleanest solution.

 var container = document.getElementById('app'); var demoApp = Elm.RenderDemo.embed(container); var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; //Cross browser support var myPerfectlyTimedFunc = function(cellID) { requestAnimationFrame(function() { if(document.getElementById('cell:' + cellID) === null) { window.alert("Cannot find cell " + cellID) } }) } demoApp.ports.onCellAdded.subscribe(myPerfectlyTimedFunc); 

See here for setting up a SPA type with multiple pages and the need to re-display the JS interop'd graph . It also has the ability to update the value of the data inside the chart. (Console log messages can also be helpful.)

If you are interested in how this can be implemented on the Elm side instead of the html / js side, see the Elm Defer Command library .

As you described, the problem is this:

  • Javascript loads and searches for an element that has not yet been created.
  • Elm displays the DOM after this search, and the item you need appears.
  • Any Elm commands that you send through the port will also be executed in or before the render, so any javascript called by subscribing to the port will have the same problem.

Elm uses requestAnimationFrame (rAF) in itself as a way to handle the DOM queue, and not without reason. Let's say Elm does a few DOM manipulations in less than 1/60 of a second, instead of processing each manipulation individually - which would be pretty inefficient - Elm will pass them to the rAF browser, which will act as a buffer / queue for general DOM rendering, In other words, view is called in the animation frame after update , so the view call will not always be executed after each update .

In the past, people would use:

 setInterval(someAnimationFunc, 16.6) //16.6ms for 60fps 

requestAnimationFrame has emerged as a way for the browser to save the queue that it controls and runs through 60 frames per second. It offers a number of improvements:

  • The browser can optimize the rendering, so the animation will be smoother
  • Animation in inactive tabs will stop, allowing the processor to cool
  • More battery

More information about rAF here , and here and the video here

My personal story began when I tried to display the chart of Chartist.js in a div, originally created in Elm. I also wanted to have multiple pages (SPA style), and the chart would have to be re-displayed when the div was recreated with various page changes.

I wrote the div as direct HTML in the index, but this prevented me from SPA functionality. I also used ports and subscriptions with jQuery ala $(window).load(tellElmToReRender) , and also provided Arrive.js a go - but each of them led to various errors and lack of desired functionality. I messed up a bit with rAF, but used it in the wrong place and wrong. This was after listening to ElmTown - Episode 4 - JS Interop , where I saw the light and realized how to really use it.

+6
source

I wanted to do something similar to this yesterday to integrate MorrisJS into Elm generated div

In the end, I came across JS Arrival , which uses the new MutationObserver , available in most modern browsers, to view the DOM for changes.

So, in my case, the code looks something like this (simplified):

 $(document).ready(() => { $(document).arrive('.morris-chart', function () { Morris.Bar({ element: this, data: [ { y: '2006', a: 100, b: 90 }, { y: '2007', a: 75, b: 65 }, { y: '2008', a: 50, b: 40 }, { y: '2009', a: 75, b: 65 }, { y: '2010', a: 50, b: 40 }, { y: '2011', a: 75, b: 65 }, { y: '2012', a: 100, b: 90 } ], xkey: 'y', ykeys: ['a', 'b'], labels: ['Series A', 'Series B'] }) }) }) 

This oversees the dom for any new elements with the .morris-chart class as soon as it creates a chart using this new element.

So, this is only called after Elm runs the view function and then re-creates the DOM.

Perhaps something like this will suit your needs.

+4
source

All Articles