Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <h2>Statechart debugger</h2>
<p>Enter a valid xstate statechart, or pick one by clicking a button, hit 'Parse' and then 'initial' to make it run!</p>
<p>
  <button id="on-off-statechart">Simple on-off statechart</button>
  <button id="trafficlights-statechart">Traffic lights statechart (guards)</button>
  <button id="crazy-statechart">Kitchen sink statechart</button>
  <a href="https://codepen.io/mogsie/full/YapZjZ#" id="permalink" target="_top">Permalink to your statechart</a>
</p>
<textarea id="input" placeholder="Enter statechart here, as a json literal"></textarea>
<br>
<textarea id="rundown-input" placeholder="Enter Rundown here (optional, see below)"></textarea>
<br>
<button title="Restart the rundown from the beginning" id="playback">Start rundown</button>
<button title="Do the next thing in the rundown" id="playback-next">>|</button>
<button title="Play the rundown to the end" id="playback-all">>>></button>
<button id="playback-pause">II</button>
<p>
  <button id="parse">Parse</button> will parse the model and make the debugger ready for you to send the <button id="initial">initial</button> event.
</p>
<div id="error"></div>
<div id="container">
  <div id="previous-state" class="previous">
    <h4 title="This is the state of the machine prior to the last event">Previous state</h4>
    <div class="clear content"></div>
  </div>
  <div id="previous-guards" class="previous">
    <h4 title="The guards were at this state when the last event was processed">Previous guards</h4>
    <div class="clear content"></div>
  </div>
  <div id="previous-event" class="previous">
    <h4 title="This is the last event that fired">Last event</h4>
    <div class="clear content"></div>
  </div>
  <div id="previous-activities" class="previous">
    <h4 title="Before the last event happened, the following activities were running">Previous activity set</h4>
    <div class="clear content"></div>
  </div>
  <div id="actions" class="previous">
    <h4 title="The last event reported the following actions should be executed">Last Actions</h4>
    <div class="clear content"></div>
  </div>
  <div id="state" class="current">
    <h4 title="After the last event was fired, the machine ended up in this state">Current state</h4>
    <div class="clear content"></div>
  </div>
  <div id="activities" class="current">
     <h4 title="After the last event was fired, these activities should be active">Current activities</h4>
    <div class="clear content"></div>
  </div>
  <div id="guards" class="current">
    <h4 title="Control the value of each guard by ticking the boxes.  A ticked box means that the guard condition holds">Guards</h4>
    <div class="clear content"></div>
  </div>
  <div id="event" class="current">
    <h4 title="Press these buttons in order to send a particular event to the state machine to see what happens">Trigger next event</h4>
    <div class="clear content"></div>
  </div>
  <div id="rundown" class="">
    <h4 title="The signals that are passed to the statechart are sent here.  You can copy the text and use it to replay a sequence of events.">Rundown</h4>
    <div class="clear content" id="rundown-output"></div>
  </div>
</div>

<h4>How to use</h4>
<ol>
  <li>Paste or edit an xstate statechart definition in the text area</li>
  <li>Click "parse" to parse it (buttons should appear in the "Trigger next event" area)</li>
  <li>Click "initial" to give your machine an initial state</li>
  <li>Click the buttons that appeared in "Trigger next event" to send the events to the state machine</li>
  <li>Tick the guards (if you have any) to specify if the guard conditions hold</li>
</ol>
<h4>Permalink</h4>
<p>It's possible to link to this pen pre-filled with a statechart.  Just copy or share the permalink, the URL contains the whole statechart.</p>
<h4>To use the rundown</h4>
<p>When you hit "initial" and start sending events and passing guards and so on, the buttons you press are recorded in the <i>Rundown</i>.  A rundown is essentially a history of the things that happen in a "run" of a statechart.  The rundown can be copied and pasted into the "rundown" input field; If you do so, then hit the "Play rundown" button, the debugger will perform the steps as provided in the rundown.  Rundowns can contain comments (<tt>#</tt>) and blank lines, which both be ignored. </p>
<h4>Future possibilities</h4>
<ul>
<li>Code editor (e.g. typescript editor with intellisense, or emmet-like statechart creation</li>
<li>Visualization of the whole statechart, and highlight the currently active states</li>
  <li>Highlight which guards were checked; perhaps even highlight which guards <i>will be checked</i> if an event would be fired</li>
<li>Keep a "run-down" in a little text area that allows you to playback a whole run by sending the same sequence of events / guard checks, and share them with others, etc.</li>
</ul>
              
            
!

CSS

              
                body {
  background-color: #eee
}
#input {
  max-width: 70rem;
  width: 100%;
  min-height: 20rem;
  max-height: 50%;
}
#error {
  color: #C00;
}
#container {
  max-width: 70rem;
  display: grid;
  grid-template-areas:
    "previous-state             previous-guards         state                 guards"
    "previous-state             previous-event          state                 event"
    "previous-state             actions                 state                 event"
    "previous-activities        previous-activities     previous-activities   rundown"
    "activities                 activities              activities            rundown";
  grid-template-columns: 1fr 1fr 1fr 1fr;
  grid-gap: 0.7em;
}

#container>div {
  background-color: white;
}
#container>div.current {
  background-color: #efe;
}
#container>div.previous {
  background-color: #fee;
}



#previous-state {
  grid-area: previous-state;
}

#previous-guards {
  grid-area: previous-guards;
}

#previous-event {
  grid-area: previous-event;
}

#activities {
  grid-area: activities
}

#previous-activities {
  grid-area: previous-activities
}

#state {
  grid-area: state;
}

#actions {
  grid-area: actions;
}

#event {
  grid-area: event
}

#guards {
  grid-area: guards
}

#rundown {
  grid-area: rundown
}

#input,
#rundown-input {
  font-family: sans-serif;
}
#rundown-input {
  min-width: 20rem;
  min-height: 3rem;
}

#container>div>.content,
#container>div>h4 {
  margin: 0.5rem;
}
#container>div>h4 {
  cursor: help;
  border-bottom: 1px solid rgba(0,0,0,0.2);
}
#container>div {
  border: 1px solid rgba(0, 0, 0, 0.3);
  box-shadow: 0 0 3px rgba(0, 0, 0, 0.2)
}

#event button,
#guards label {
  display: block;
}

#container .content {
  white-space: pre;
}

#rundown .content {
  overflow: scroll;
  max-height: 10rem;
}
              
            
!

JS

              
                console.clear();

function error(value) {
  document.getElementById("error").innerHTML = value;
}

function addToRundown(event, text) {
  const output=document.getElementById('rundown-output');
  output.textContent += event + (text? (' ' + encodeURIComponent(text)) : "") + '\n';
  output.scrollTop = output.scrollHeight;
}

// Hack: Make it so that you can include statecharts in the #
if (window.location.search && window.location.search.indexOf('sc=') != -1) {
  try {
    const sc = decodeURIComponent(
      window.location.search.substring(1)
        .split('&')
        .find(v => v.substring(0,3) == 'sc=')
        .substring(3)
    );
    const rd = decodeURIComponent(
      (window.location.search.substring(1)
        .split('&')
        .find(v => v.substring(0,3) == 'rd=') || '')
        .substring(3)
    ) || '';
  
    document.getElementById('input').value = sc;
    document.getElementById('rundown-input').value = rd;
  }
  catch (e) {
    console.log('error while parsing hash', e)
  }
}

document.getElementById('input').oninput = () => {
  document.getElementById('permalink').href = 'https://codepen.io/mogsie/full/YapZjZ/' +
    '?sc=' +
    window.encodeURI(document.getElementById('input').value).replace(/'/g, "%27").replace(/#/g, "%23") +
    '&rd=' +
    window.encodeURI(document.getElementById('rundown-input').value).replace(/'/g, "%27").replace(/#/g, "%23");
  // 
}
document.getElementById('rundown-input').oninput = document.getElementById('input').oninput;

// fake a call to onInput on load!
document.getElementById('input').oninput()

// A record of which conditionals are set <string,boolean>
// a checkbox is created which keeps this record up-to-date whenever it is ticked/unticked.
// The guards are rewritten to inspect this bitset instead of executing the guard...
let conditionals = {}

function addConditional (name) {
  if (conditionals[name] !== undefined) {
    return;
  }
  conditionals[name] = false;
  checkbox = document.createElement('input');
  checkbox.type = 'checkbox'
  checkbox.id = 'guard_'+ encodeURIComponent(name);
  checkbox.value = name;
  checkbox.onclick = (e) => {
    conditionals[e.srcElement.value] = !!e.srcElement.checked;
    addToRundown('guard ' + conditionals[e.srcElement.value], name)
  }
  label = document.createElement('label')
  label.append(checkbox);
  label.append(' ' + name);
  document.querySelector('#guards .content').append(label);
}

// recursively walks through the entire state hierarchy and replaces cond: fn with our own function.
function replaceConditionals(conf) {
  if (conf.states) {
    Object.keys(conf.states).forEach(state => replaceConditionals(conf.states[state]));
  }
  if (conf.on) {
    Object.keys(conf.on)
        // enter the 'conf.on'
        .map(event => conf.on[event])
        // ignore string events
        .filter(transition => typeof transition !== 'string')
        // convert non-arrays into arrays
        .map(transition => Array.isArray(transition) ? transition : Object.keys(transition).map(event => transition[event]) )
        // join all the arrays
        .reduce((a,b)=>[...a, ...b], [])
        // ignore anyone with no cond statements
        .filter(transition => !!transition.cond)
        .forEach(transition => {
      const conditional = transition.cond.toString();
      addConditional(conditional);
      transition.cond = () => conditionals[conditional];
    });
  }
  return conf;
}

const initial = () => send();
document.getElementById("parse").onclick = parse;
document.getElementById("playback").onclick = start_playback;
document.getElementById("playback-next").onclick = playback_next;
document.getElementById("playback-all").onclick = playback_all;
document.getElementById("playback-pause").onclick = playback_pause;
document.getElementById("initial").onclick = initial;

let currentState = "";
let send;

const playback_lines = [];

function playback_next() {
  if (playback_lines) {
    console.log(playback_lines)
    playback_line(playback_lines.shift());
    return true;
  }
  return false;
}

let next_playback = undefined;

function playback_pause() {
  if (next_playback) {
    clearTimeout(next_playback);
  }
}

function playback_all() {
  next_playback = setTimeout(() => {
    if (playback_next()) {
      playback_all();
    }
  },200);
}

function playback_line(line) {
  console.log("playing back", line)
  const event = line.split(' ')[0];
  if (event === 'initial') {
    send();
  }
  if (event === 'guard') {
    const value = line.split(' ')[1]
    const guard = line.split(' ')[2]
    const checkbox = document.getElementById('guard_' + guard);
    if (! checkbox) {
      error('Guard ' + guard + ' not found.');
      return;
    }
    if (checkbox.checked && value == 'false') {
      checkbox.click();
    }
    if (! checkbox.checked && value == 'true') {
      checkbox.click();
    }
  }
  if (event === 'event') {
    const value = line.substring('6')
    send(value);
  }  
}

function start_playback() {
  if (! parse()) return;
  document.getElementById("rundown-input").value.split('\n')
    .map(line => line.trim())
    .filter(line => line.length > 0)
    .filter(line => line[0] != '#')
    .forEach(line => playback_lines.push(line));
}

function parse() {
  // clear out all generated content from previous runs
  document.querySelectorAll(".clear").forEach(item=>item.innerHTML="");
  document.getElementById("error").innerHTML="";
  // also zero out the conditionals!
  conditionals = {};
// json  var machine = xstate.Machine(JSON.parse(document.getElementById("input").value));
  var conf;
  try {
    conf = eval('()=>(' + document.getElementById("input").value + ')');
  }
  catch (e) {
    error(e.toString());
    return false;
  }
  try {
    if (conf) {
      conf = conf();
    }
  }
  catch (e) {
    error(e.toString());
    return false;
  }
  if (! conf) return false;
  replaceConditionals(conf);
  var machine = xstate.Machine(conf);
  
  currentState = "";
  send = (event) => {
    // If you re-click the initial button, zero out the current state.
    if (event === undefined) {
      addToRundown('initial');
      currentState = {};
    }
    else {
      addToRundown('event', event);
    }
    document.querySelector("#previous-state .content").innerHTML = JSON.stringify(currentState.value,null,2);
    document.querySelector("#previous-guards .content").innerHTML = JSON.stringify(conditionals,null,2);
    document.querySelector("#previous-event .content").innerHTML = event && JSON.stringify( event,null,2 )|| 'initial';
    document.querySelector("#previous-activities .content").innerHTML = JSON.stringify(currentState.activities,null,2);
    try {
      if (event === undefined) {
        currentState = machine.initialState;
      }
      else {
        currentState = machine.transition(currentState, event);
      }
    }
    catch (e) {
      document.getElementById("error").innerHTML=e.toString();
      return;
    }
    document.querySelector("#state .content").innerHTML = JSON.stringify(currentState.value,null,2);
    document.querySelector("#activities .content").innerHTML = JSON.stringify(currentState.activities,null,2);
    document.querySelector("#actions .content").innerHTML = JSON.stringify(currentState.actions,null,2);
    document.querySelectorAll("#event .content button").forEach(button => button.disabled = false);

  };
  document.querySelector('#event .content').innerHTML="";
  machine.events.forEach(state => {
    const button = document.createElement("button");
    button.append(state);
    button.disabled = true;
    button.onclick = e => send(state);
    document.querySelector('#event .content').append(button);
  });
  return true;
}




const textarea = document.getElementById('input');
document.getElementById("on-off-statechart").onclick = () => {
  textarea.value = `
{
  initial: 'on',
  states: {
    on: {
      on: {
        flick: 'off'
      }
    },
    off: {
      on: {
        flick: 'on'
      }
    }
  }
}`
};


document.getElementById("crazy-statechart").onclick = () => {
  textarea.value = `
{
  initial: "xyzzy",
  key: "my_machine",
  states: {
    xyzzy: {
      on: {
        event2:"foo"
      },
 
      initial: 'x',
      states: {
        x: {
          on: { 'xy': 'y'},
          activities: ["xxx" ]
        },
        y: {
          on: { 'xy': 'x' },
          activities: ["xxy" ]
        },
      }  
    },
    foo: {
      onEntry: ['do_something'],
      on: {
        event2:"bar",
        event3: {"bar": {
          cond: (something) => something == foo + 44,
        }},
        event4: [{
          cond: (something) => something == bar,
          target: "bar"
        },{
          actions:"out-of-event4",
          target: "bar"
        }]
      },
      parallel: true,
      states: {
        xx: {
          activities: ["fooing" ],
          initial: 'x',
          states: {
            x: {
              on: { 'xy': 'y' },
              activities: ["xxx" ]
            },
            y: {
              on: { 'xy': 'x' },
              activities: ["xxy" ]
            },
          }  
        },
        yy: {
          activities: ["whying" ],
          initial: 'x',
          states: {
            x: {
              on: { 'xy': 'y' },
              activities: ["yyx" ]
            },
            y: {
              on: { 'xy': 'x' },
              activities: ["yyyy" ]
            },
          }  
        }
      }
    },
  
    bar:{
      on: {
        event2:"foo",
        event3:"foo"
      }
    }
  }
}
`
};


document.getElementById("trafficlights-statechart").onclick = () => {
  textarea.value = `
{
  key: 'light',
  initial: 'green',
  states: {
    green: {
      on: {
        TIMER: {
          yellow: {
            cond: fullState => fullState.elapsed >= 30000
          }
        }
      }
    },
    yellow: {
      on: {
        TIMER: {
          red: {
            cond: fullState => fullState.elapsed >= 5000
          }
        }
      }
    },
    red: {
      on: {
        TIMER: {
          green: {
            cond: fullState => fullState.elapsed >= 30000
          }
        }
      }
    }
  }
}`};
              
            
!
999px

Console