-
Notifications
You must be signed in to change notification settings - Fork 16
Running headless Elm
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.
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
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.
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