There's a tutorial on what state machines are elsewhere; this page is on using this library.
Like many modern Javascript libraries, JSSM is available in many builds, on NPM,
on Github Packages, and from CDN. JSSM is packaged as an es6 module
for
modern node, modern browsers, and packagers; as a commonjs module
for node
back to 2018 and older bundlers; and as an iife
for classic browsers. JSSM
also ships with typescript support, and full documentation.
Generally, you should be able to use the system you're used to, in whatever
environment you're used to - be that include
or require
or a <script>
tag,
in node, browser, typescript, deno, es6 environments, es5 environments, modern
stuff, ancient stuff, whatever - and it should Just Work ™.
This tutorial works from CDN. The next tutorial goes over how to work with various environments, builds, and so on.
To start with, let's do things the sloppy, "just run already" way. We'll load the library directly in the HTML, from CDN.
<!doctype html>
<html>
<head>
<script type="text/javascript"
src="https://cdn.jsdelivr.net/npm/jssm/dist/jssm.es5.iife.min.js">
</script>
</head>
</html>
At this point, you can already play with the library, in the developer console.
First, we need a toy traffic light. Here's some HTML structure:
<!doctype html>
<html>
<head>
<title>Traffic light example</title>
</head>
<body>
<table id="light" class="light_off">
<tr><td id="red"><span></span></td></tr>
<tr><td id="yellow"><span></span></td></tr>
<tr><td id="green"><span></span></td></tr>
</table>
</body>
</html>
And a bit of CSS to get it to look just so:
<style type="text/css">
#light { border-collapse: collapse; } /* don't separate cells */
#light td { border: 2px solid #e3a31d; } /* mildly darker orange border around cells */
#light span {
height : 4em; /* size the lightbulb */
width : 4em; /* size the lightbulb */
border : 2px solid black; /* looks weird without an edge */
border-radius : 50%; /* make it round */
display : inline-block; /* so that it will lay out margins correctly */
margin : 0.5em; /* space around bulb */
}
#red span { background-color: #300; } /* very dark when not lit */
#yellow span { background-color: #220; }
#green span { background-color: #030; }
.light_red #red span { background-color: #F00; } /* bright when lit */
.light_yellow #yellow span { background-color: #EE0; }
.light_green #green span { background-color: #0F0; }
td { background-color: #FCC550; } /* that yellow-slightly-orange frame */
</style>
We'll also add a bit of Javascript to make it usable.
<script type="text/javascript">
function light(what) {
if (['red','yellow','green','off'].includes(what)) {
document.getElementById('light').className = `light_${what}`;
}
}
</script>
End result should look a bit like this:
Next, let's have the machine and the UI interact a bit.
If you pull the CSS out from the previous example into a file called tl.css
and otherwise assume it hasn't changed, you're left with this:
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" href="tl.css" />
<script type="text/javascript"
src="./jssm.es5.iife.js"></script>
<script type="text/javascript">
function set_color(what) {
if (['red','yellow','green','off'].includes(what)) {
document.getElementById('light').className = `light_${what}`;
}
}
window.onload = () => {
const traffic_light = sm`
Red 'next' => Green 'next' => Yellow 'next' => Red;
`;
};
</script>
</head>
</html>
We'll add a simple "hook," which means the state machine will call functions you provide when things happen. In this case, we'll call the hook whenever any state is entered.
Hooks take an object which includes, among other things, the state being
transitioned from
and the state being transitioned to
. In this example, the
latter is exactly what we want, so, we'll just destructure it right off.
traffic_light.hook_any_transition( ({ to }) => set_color(to) );
We will also, since we're working in the console for now, we'll export the
variable onto window
so that we can use it easily in the console.
window.tl = traffic_light;
Both these lines go at the end of onload
, which now looks like this:
window.onload = () => {
const traffic_light = window.jssm.sm`
red 'next' => green 'next' => yellow 'next' => red;
`;
traffic_light.hook_any_transition( ({to}) => set_color(to) );
window.tl = traffic_light;
};
And now, they're linked.
Of course, we wouldn't have users use the console; let's have some widgets wired
up. Also, while we're at it, let's decide what to do about the light being
off
.
Realistically, a light can turn off - the power can go out, they can down it for maintenance, it might be new, et cetera; so, a practical machine should cover being turned off. Let's also.
Our new machine:
const traffic_light = sm`
off 'enable' -> red;
red 'next' => green 'next' => yellow 'next' => red;
[red yellow green] 'disable' -> off;
`;
We've a convention here. Putting several names in []
square brackets makes a
"list," and when we make an arrow from the list, it actually makes a distinct
arrow for each element in the list. So, the line
[red yellow green] 'disable' -> off;
actually makes three transitions, and gives them all the same action.
The state machine will now start in off
, because unless you specify otherwise,
the first named state is assumed to be the starting state.
We'll need to add two labelled containers to our UI - one for the available
actions, and one for the available transitions. Those might initially just be
empty <div>
s, and look like this:
<div id="avail_actions"></div>
<div id="avail_transitions"></div>
Which actions and transitions are available at any given time on this machine change, and we don't want to have to manage knowing what's going on, so we'll just dynamically create and destroy whatever the machine says is available currently, on each transition.
To update the action buttons, list the actions exiting the current state with
machine.list_exit_actions()
:
function update_action_buttons() {
const container = document.getElementById('avail_actions');
container.innerHTML = '';
traffic_light.list_exit_actions().forEach( ea => {
const newButton = document.createElement('button');
newButton.innerHTML = ea;
newButton.onclick = () => traffic_light.action(ea);
container.appendChild(newButton);
} );
}
And almost identical, to update the transition buttons, list the relevant
exiting transitions with machine.list_exits()
:
function update_transition_buttons() {
const container = document.getElementById('avail_transitions');
container.innerHTML = '';
traffic_light.list_exits().forEach( et => {
const newButton = document.createElement('button');
newButton.innerHTML = et;
newButton.onclick = () => traffic_light.action(et);
container.appendChild(newButton);
} );
}
Finally, we call both updates in an entry hook, as well as when the webpage is being set up initially:
traffic_light.hook_entry( () => {
update_action_buttons();
update_transition_buttons();
} );
window.onload = () => {
// ...
update_action_buttons();
update_transition_buttons();
};
Generated using TypeDoc