Skip to content

Instantly share code, notes, and snippets.

@sukima
Last active May 9, 2024 23:51
Show Gist options
  • Save sukima/43d64f6c96d05a40393d21df09107d21 to your computer and use it in GitHub Desktop.
Save sukima/43d64f6c96d05a40393d21df09107d21 to your computer and use it in GitHub Desktop.
XState Decorators
export const CURRENT_STATE = Symbol('current state');
export const STATE_PROPS = Symbol('state properties');
export const MACHINE = Symbol('machine');
export const LISTENERS = Symbol('listeners');
export const CONFIG = Symbol('config');
export const CONTEXT = Symbol('context');
import Controller from '@ember/controller';
import { useMachine, machineState, transition } from '../ember-xstate';
const { Machine } = XState;
// Only one machine per class. If you want more then manage multiple EmberObjects with machines and/or dive into XState itself.
@useMachine(Machine({
id: 'example',
initial: 'green',
states: {
'green': {
on: { SWITCH: 'yellow' }
},
'yellow': {
on: { SWITCH: 'red' }
},
'red': {
on: { SWITCH: 'green' }
}
}
}))
export default class ApplicationController extends Controller {
@machineState myState;
@transition
switch(send) {
send('SWITCH');
}
}
import {
CURRENT_STATE,
STATE_PROPS,
MACHINE,
INTERPRETER,
LISTENERS,
CONFIG,
CONTEXT
} from './-private';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { stateComparators } from './utils/state-comparators';
const { interpret } = XState;
export function useMachine(machine) {
return function decorator(Klass) {
class EmberXstateWrapper extends Klass {
constructor() {
super(...arguments);
this[MACHINE] = machine
.withContext(this[Klass[CONTEXT]]?.() ?? machine.context)
.withConfig(this[Klass[CONFIG]]?.() ?? {});
this[INTERPRETER] = interpret(this[MACHINE])
.onTransition(state => {
this[CURRENT_STATE] = {
...state,
valueStrings: state.toStrings().join(' '),
in: stateComparators(state.value)
};
Klass[STATE_PROPS]?.forEach(prop => {
this[prop] = this[CURRENT_STATE];
});
});
Klass[LISTENERS]?.forEach(method => {
this[INTERPRETER].onTransition(state => this[method](state));
});
this[INTERPRETER].start();
}
willDestroy() {
super.willDestroy(...arguments);
this[INTERPRETER].stop();
}
}
return EmberXstateWrapper;
};
}
export function machineState(target, name, descriptor) {
target.constructor[STATE_PROPS] = target.constructor[STATE_PROPS] ?? [];
target.constructor[STATE_PROPS].push(name);
return tracked(target, name, descriptor);
}
export function withConfig(target, name, descriptor) {
target.constructor[CONFIG] = name;
return descriptor;
}
export function withContext(target, name, descriptor) {
target.constructor[CONTEXT] = name;
return descriptor;
}
export function onTransition(target, name, descriptor) {
target.constructor[LISTENERS] = target.constructor[LISTENERS] ?? [];
target.constructor[LISTENERS].push(name);
return descriptor;
}
export function transition(target, name, descriptor) {
let originalFn = descriptor.value;
descriptor.value = function(...args) {
return originalFn.call(this, this[INTERPRETER].send, ...args);
}
return action(target, name, descriptor);
}
import { helper } from '@ember/component/helper';
import { INTERPRETER } from '../-private';
export function transition([target, ...curryArgs]) {
return function(...args) {
return target[INTERPRETER].send(...curryArgs, ...args);
};
}
export default helper(transition);
body {
background-color: #eee;
}
code: {
text-shadow: 4px 4px 4px 4px #000;
}
[data-state~="green"] {
color: green;
}
[data-state~="yellow"] {
color: yellow;
}
[data-state~="red"] {
color: red;
}
<p>
State via tracked state:
<code data-state={{this.myState.valueStrings}}>
{{this.myState.valueStrings}}
</code>
</p>
<button type="button" {{on "click" this.switch}}>
Switch via decorator
</button>
<button type="button" {{on "click" (transition this "SWITCH")}}>
Switch via helper
</button>
{{#if this.myState.in.red}}
<h2>STOP!</h2>
{{/if}}
{{#if this.myState.in.green}}
<h4>Go</h4>
{{/if}}
{
"version": "0.17.0",
"EmberENV": {
"FEATURES": {},
"_TEMPLATE_ONLY_GLIMMER_COMPONENTS": false,
"_APPLICATION_TEMPLATE_WRAPPER": true,
"_JQUERY_INTEGRATION": true
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js",
"xstate": "https://unpkg.com/xstate@4/dist/xstate.js",
"ember": "3.17.0",
"ember-template-compiler": "3.17.0",
"ember-testing": "3.17.0"
},
"addons": {
"@glimmer/component": "1.0.0"
}
}
import { typeOf } from '@ember/utils';
export function stateComparators(stateValue) {
let foundKeys = [];
function recurse(states) {
if (typeOf(states) === 'string') {
foundKeys.push(states);
return { [states]: true };
}
let comparator = {};
for (let [key, value] of Object.entries(states)) {
foundKeys.push(key);
comparator[key] = recurse(value);
}
return comparator;
}
let top = recurse(stateValue);
for (let key of foundKeys) {
top[key] = top[key] ?? true;
}
return top;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment