Sukima5th December 2019 at 9:00pm
Super simple Finite State Machine. This attempts to follow the conventions of XState with the exceptions of multiple actions, interpreter, and the added concept of meta content.
See also, CSS based view states.
function createMachine(
{ context = {}, id, initial, states = {}, on = {} } = {},
{ actions = {}, guards = {} } = {}
) {
function stateFor(stateEvent, stateContext = context, changed) {
let { target } = stateEvent;
if (!target) { return; }
let { entry, meta = {} } = states[target] || {};
let newContext = { ...stateContext };
let nextState = { value: target, meta, context: newContext, changed };
nextState.actions = changed
? [].concat(entry || []).map(boundAction('entry', stateEvent, nextState))
: [];
return nextState;
}
function findStateEvent(transitionData, eventData, stateContext = context) {
for (let transition of [].concat(transitionData)) {
if (!transition) { continue; }
let { target = transition, cond = () => true } = transition;
if (typeof target !== 'string') { target = ''; }
let stateEvent = { ...eventData, target };
if (typeof cond === 'function') {
if (cond(stateContext, stateEvent)) { return stateEvent; }
continue;
}
if (guards[cond](stateContext, stateEvent)) { return stateEvent; }
}
return {};
}
function boundAction(type, event, state) {
return function(action) {
if (!action) { return; }
if (!action.exec) { action = { exec: action }; }
if (typeof action.exec !== 'function') { action.exec = actions[action.exec]; }
let exec = action.exec && ((...args) => action.exec(state.context, event, { action, state }, ...args));
return { type: (action.type || type), exec };
};
}
return {
id, initialState: stateFor({ target: initial, type: '#init' }, context, true),
transition(state, transitionEvent) {
let currentValue = (state && state.value) || state;
let stateConfig = states[currentValue];
if (!stateConfig) {
throw new Error(`State '${currentValue}' not found on machine${id ? ` ${id}` : ''}`);
}
let { type = transitionEvent, value } = transitionEvent;
let transitions = { ...on, ...(stateConfig.on || {}) };
let transition = transitions[type] || {};
let stateEvent = findStateEvent(transition, { type, value }, state.context);
let nextState = stateEvent.target && stateEvent.target !== ''
? stateFor(stateEvent, state.context, true)
: stateFor({ target: currentValue }, state.context, false);
nextState.actions = [
...(nextState.changed
? [].concat(stateConfig.exit || []).map(boundAction('exit', stateEvent, nextState))
: []),
...[].concat(transition.actions || []).map(boundAction('action', stateEvent, nextState)),
...nextState.actions,
];
nextState.actions = nextState.actions.filter(action => !!action);
return nextState;
},
};
}
Tests are available for this snippet.