Skip to content

Instantly share code, notes, and snippets.

@ChrisShank
Last active May 2, 2021 02:52
Show Gist options
  • Save ChrisShank/6943d62f4283b6c027c4cdb2612cdc2f to your computer and use it in GitHub Desktop.
Save ChrisShank/6943d62f4283b6c027c4cdb2612cdc2f to your computer and use it in GitHub Desktop.
import { inspect } from '@xstate/inspect';
import {
assign,
createMachine,
interpret,
sendParent,
spawn,
actions,
send,
SpawnedActorRef,
} from 'xstate';
import { createModel } from 'xstate/lib/model';
inspect({ iframe: false });
type Key = 'ArrowRight' | 'ArrowLeft' | 'ArrowUp' | 'ArrowDown' | 'x';
function createKeyMachine(key: Key, x: number, y: number) {
const keyModel = createModel(
{ x, y },
{
events: {
release: () => ({}),
press: () => ({}),
focus: () => ({}),
unfocus: () => ({}),
move: (dx: number, dy: number) => ({ dx, dy }),
},
}
);
const id = `key-${key}`;
const keyMachine = createMachine<typeof keyModel>({
id,
context: keyModel.initialContext,
type: 'parallel',
states: {
press: {
initial: 'up',
states: {
up: {},
down: {
on: { release: 'up' },
},
},
on: {
press: { target: '.down', actions: sendParent({ type: 'press', key }) },
},
},
focus: {
initial: 'unfocused',
states: {
unfocused: {
on: { focus: 'focused' },
},
focused: {
on: {
unfocus: 'unfocused',
move: {
actions: assign({
x: (context, event) => context.x + event.dx,
y: (context, event) => context.y + event.dy,
}),
},
},
},
},
},
},
invoke: {
id: 'keypress',
src: () => (sendBack) => {
// Once the correct key is pressed, only send back an event on every animation frame
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === key) {
sendBack(keyModel.events.press());
}
};
window.addEventListener('keydown', onKeyDown);
const onKeyUp = (e: KeyboardEvent) => {
if (e.key === key) {
sendBack(keyModel.events.release());
}
};
window.addEventListener('keyup', onKeyUp);
return () => {
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('keyup', onKeyUp);
};
},
},
});
return keyMachine;
}
const appModel = createModel(
{
focusedKey: 'x' as Key,
// Pretend actors exist since they are assigned in the entry action to register with
ArrowUp: (null as unknown) as SpawnedActorRef<any, any>,
ArrowDown: (null as unknown) as SpawnedActorRef<any, any>,
ArrowLeft: (null as unknown) as SpawnedActorRef<any, any>,
ArrowRight: (null as unknown) as SpawnedActorRef<any, any>,
x: (null as unknown) as SpawnedActorRef<any, any>,
},
{
events: {
press: (key: Key) => ({ key }),
},
}
);
const appMachine = createMachine<typeof appModel>({
id: 'app',
context: appModel.initialContext,
entry: [
assign({
ArrowUp: (context) => spawn(createKeyMachine('ArrowUp', 0, 0), 'ArrowUp'),
ArrowDown: (context) => spawn(createKeyMachine('ArrowDown', 0, 0), 'ArrowDown'),
ArrowLeft: (context) => spawn(createKeyMachine('ArrowLeft', 0, 0), 'ArrowLeft'),
ArrowRight: (context) => spawn(createKeyMachine('ArrowRight', 0, 0), 'ArrowRight'),
x: (context) => spawn(createKeyMachine('x', 0, 0), 'x'),
}),
send('focus', { to: (context) => context[context.focusedKey] }),
],
initial: 'idle',
states: {
idle: {
on: {
press: {
actions: actions.pure((context, event) => {
const { focusedKey } = context;
if (event.key !== 'x') {
const dx = event.key === 'ArrowLeft' ? -1 : event.key === 'ArrowRight' ? 1 : 0;
const dy = event.key === 'ArrowDown' ? -1 : event.key === 'ArrowUp' ? 1 : 0;
return send({ type: 'move', dx, dy }, { to: () => context[focusedKey] });
}
const nextFocusedKey = getNextKey(focusedKey);
return [
send('unfocus', { to: () => context[focusedKey] }),
assign({ focusedKey: nextFocusedKey }),
send('focus', { to: () => context[nextFocusedKey] }),
];
}),
},
},
},
},
});
function getNextKey(key: Key): Key {
if (key === 'x') return 'ArrowLeft';
else if (key === 'ArrowLeft') return 'ArrowUp';
else if (key === 'ArrowUp') return 'ArrowRight';
else if (key === 'ArrowRight') return 'ArrowDown';
return 'x';
}
const service = interpret(appMachine, { devTools: true });
service.start();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment