So I've come up with this embedded domain specific language (DSL) for writing state machines in JavaScript. The DSL has a fluent interface that builds up, internally, expressions of linear temporal logic (think of these as regular expressions). The expressions are then translated into state machines. The translation phase binds event listeners to nodes in the document object model (DOM), which will notify the state machine of new inputs and may cause a state transition. The programmer can therefore specify and at the same time implement stateful behaviour concisely and often with a single statement of the DSL.
The DSL is defined as a RequireJS module which, like JQuery, has a single export that is a function and entry point into the DSL. The examples are wrapped around a RequireJS import: require(['ayttm.min'], function(_){ ... }); which binds the DSL's entry function to _. The file ayttm.min.js contains 34K of minified JavaScript source code for the DSL and has no dependencies on third party APIs.
This example refers to the following HTML elements: <div id="container"> <div id="target"> <div> <div> which are rendered below:
- The inner div can be moved by dragging the mouse.
- Hover intent is detected on the inner div (n.b. Requires IE 10)
- The inner div can be rotated by hover and holding down the shift key.
Moving the box by dragging the mouse.
The following statement begins by detecting a mouse-down event on the target. define(['example1'], function(){ require(['ayttm.min'], function(_){ _('#target').mousedown(). andNext(). _('#container').mousemove(). with(_.mousemoveTracker('#target', '#container')). until(). _('#target').mouseup(). or(). _('#container').mouseleave().__(); }); }); Mouse-move events on the container then trigger a callback i.e. mousemoveTracker, which updates the CSS properties, top and left, of the target, based on the x and y coordinates of the mouse-move event. The target is kept within the bounds of the container. The target will track the mouse-move events until a mouse-up event happens on the target or the mouse leaves the container. The last method call in the chain, __(), terminates the statement and binds event handlers to the HTML document. This example can also be applied to touch events with HTML 5.
Detection of hover intent by the mouse was the recommended approach for accessing mega menus and is still used on many websites today. A timer is used to prevent changing the state of the user interface too soon after a hover is detected - 500 milliseconds is used in this example to toggle a CSS class, hoverClass, on the target. define(['example2'], function(){ require(['ayttm.min'], function(_){ _([ _('#target').mouseenter(). with(_.timer(500)). andNext(). not(_('#target').mouseout()). until(). _(_.timout()). with(_.addClass("#target", "hoverClass")), _('#target').mouseout(). with(_.timer(500)). andNext(). not(_('#target').mouseenter()). until(). _(_.timout()). with(_.removeClass("#target", "hoverClass")) ]). __(); }); }); The statement consists of an array of 2 substatements, which defines a choice of 2 dual cases. The first choice handles the case when the mouse enters the target and the second handles the mouse leaving the target. The callback, _.timeout(500), is triggered on a mouse-enter or mouse-leave event and starts a timer with a countdown of 500ms. The timeout is expected to happen later on in both substatements with the declaration of the event _.timeout(). The callback, _.timout(500), clears any timer that has already started and reschedules a new timer, so that only 1 timer is active at any moment.
Rotating the box with mouse and key events.
This example rotates the target with mouse movement while holding down the shift key. There are 2 dual cases. One of them starts by hovering the target. The other starts by holding down the shift key. The callback, rotate, adjusts the CSS 3 style, transform (in degrees), of the target, setting it to the difference (of pixels), between the y-coordinates of the current and previous mouse-move events. define(['example3'], function(){ require(['ayttm.min'], function(_){ var rotate = (function(){ var d = document.querySelector('#target'); var deg = 0; var rotate = function(e) { deg = deg + (e.source.clientY - (e.previousEvent.clientY == undefined? e.source.clientY: e.previousEvent.clientY)); d.style.webkitTransform = 'rotate(' + deg + 'deg)'; d.style.mozTransform = 'rotate('+deg+'deg)'; d.style.msTransform = 'rotate('+deg+'deg)'; d.style.oTransform = 'rotate('+deg+'deg)'; d.style.transform = 'rotate('+deg+'deg)'; } return rotate; })(); _([ _('body').shiftkeydown(). andNext(). _('#container').mousemove(). until(). _('#target').mousemove(). andNext(). _([_('#container').mousemove().with(rotate), _('#target').mousemove(), _('#target').mouseleave()]). until(). _('body').shiftkeyup() , _('#target').mousemove(). andNext(). not(_('#target').mouseleave()). until(). _('body').shiftkeydown(). andNext(). _([_('#container').mousemove().with(rotate), _('#target').mousemove(),_('#target').mouseleave()]). until(). _('body').shiftkeyup() ]). __(); }); });
The previous 3 examples are run together on the same HTML elements, without changing the code for each individual example. The main requireJS file for this page simply loads the 3 examples from above as follows: require(['example1', 'example2', 'example3'], function(){ });
This final example uses a widget that suggests a list of countries in a drop down menu while the user types in to an input text field:
The callback updates a Knockout model with a list of countries filtered by the search input. The key-down event on the input field is filtered for specific key values, i.e. certain keys such as enter and up/down arrow keys are used for controlling the widget and are ignored.
The library is implemented in TypeScript and the source is available in github.