Skip to content

Running headless Elm

Noah edited this page Nov 14, 2015 · 2 revisions

Running non-graphical Elm projects

There are three ways of running Elm apps:

  • Elm.fullscreen
  • Elm.embed
  • Elm.worker

Elm.fullscreen and Elm.embed will run an Elm application in a given DOM element. Elm.worker will create a headless instance of an Elm application.

Elm.fullscreen = function(module, args)
{
    return init(Display.FULLSCREEN, container, module, args || {});
};

Elm.embed = function(module, container, args)
{
    var tag = container.tagName;
    if (tag !== 'DIV')
    {
        throw new Error('Elm.node must be given a DIV, not a ' + tag + '.');
    }
    return init(Display.COMPONENT, container, module, args || {});
};

Elm.worker = function(module, args)
{
    return init(Display.NONE, {}, module, args || {});
};

In Elm, the main function is special. It is hardcoded to look for either the Core's Graphics modules, or VirtualDom from evancz's virtual-dom implementation, which is most commonly used with elm-html.

var signalGraph = Module.main;

// make sure the signal graph is actually a signal & extract the visual model
if (!('notify' in signalGraph))
{
    signalGraph = Elm.Signal.make(elm).constant(signalGraph);
}
var initialScene = signalGraph.value;

// Figure out what the render functions should be
var render;
var update;
if (initialScene.props)
{
    var Element = Elm.Native.Graphics.Element.make(elm);
    render = Element.render;
    update = Element.updateAndReplace;
}
else
{
    var VirtualDom = Elm.Native.VirtualDom.make(elm);
    render = VirtualDom.render;
    update = VirtualDom.updateAndReplace;
}

This means that you can only run Elm applications through Elm.embed/Elm.fullscreen, as the call to initGraphics is what runs the main function -

if (display !== Display.NONE)
{
   var graphicsNode = initGraphics(elm, Module);
}

Fortunately, when Elm is run through a worker, ports still work!

function addReceivers(ports)
{
    if ('title' in ports)
    {
        if (typeof ports.title === 'string')
        {
            document.title = ports.title;
        }
        else
        {
            ports.title.subscribe(function(v) { document.title = v; });
        }
    }
    if ('redirect' in ports)
    {
        ports.redirect.subscribe(function(v) {
            if (v.length > 0)
            {
                window.location = v;
            }
        });
    }
}
addReceivers(elm.ports);

which means that whenever we want to run code on startup, we simply make an outgoing port which trigger something else.

Running the Elm runtime

In order to actually run anything, the Elm runtime needs to be called. It's trivial to do this - simply append Elm.worker(Elm.Main); to elm.js, where Elm.Main is the name of your application file containing the ports

Other ways

If you really wanted to, you could create a document global object, then create an Elm application like so

var host = {
    tagName: 'div'
};

Elm.embed(Elm.Main, host);

You would then need to implement a Native module expanding on Elm.Native.VirtualDom, providing two core functions - render and updateAndReplace. Note that these could be overwritten by your module very easily.

VirtualDom spec

These functions are pretty losely defined, but the general gist:

  • render should take some model (referred to as "scene") and return a "node"
  • updateAndReplace should return a "node"
  • update should take a node, current "scene" and next "scene", and return a node

Sound familiar, right?

function updateAndReplace(node, curr, next)
{
    var newNode = update(node, curr, next);
    if (newNode !== node)
    {
        node.parentNode.replaceChild(newNode, node);
    }
    return newNode;
}
function render(model)
{
    update(div, model, model);
    return div;
}
function update(node, curr, next){
    return node;
}

This would allow you to model your application by having the Elm code look something like

-- MyApp has native extensions
-- which implement Elm.Native.VirtualDom
-- along with the required functions
import MyApp

-- may need a helper here like from meta-elm
-- for "converting" the type of objs at runtime
-- while allowing the type checker to sleep easy
main = MyApp.run