Last active
November 14, 2021 15:25
-
-
Save cevr/8f984684fd24be94ec5c4d1c8a6f263c to your computer and use it in GitHub Desktop.
xstate fp utils POC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as x from './xstate-fp'; | |
export enum EditableTextState { | |
idle = 'idle', | |
editing = 'editing', | |
} | |
export enum EditableTextEvent { | |
mouseenter = 'mouseenter', | |
mouseleave = 'mouseleave', | |
blur = 'blur', | |
focus = 'focus', | |
edit = 'edit', | |
change = 'change', | |
save = 'save', | |
cancel = 'cancel', | |
} | |
interface EditableTextMachineContext { | |
initialValue: string; | |
value: string; | |
actionsAreVisible: boolean; | |
} | |
const resetValue = x.assign<EditableTextMachineContext>({ | |
value: (context) => context.initialValue, | |
}); | |
const showActions = x.assign<EditableTextMachineContext>({ | |
actionsAreVisible: true, | |
}); | |
const hideActions = x.assign<EditableTextMachineContext>({ | |
actionsAreVisible: false, | |
}); | |
const setValue = x.assign({ value: (_, event) => event.value }); | |
export const EditableTextMachine = x.createMachine<EditableTextMachineContext>( | |
x.on(EditableTextEvent.mouseenter, showActions), | |
x.on(EditableTextEvent.mouseleave, hideActions), | |
x.states( | |
x.state(EditableTextState.idle, x.on(EditableTextEvent.edit, EditableTextState.editing)), | |
x.state( | |
EditableTextState.editing, | |
x.on(EditableTextEvent.focus, hideActions), | |
x.on(EditableTextEvent.blur, EditableTextState.idle, resetValue), | |
x.on(EditableTextEvent.change, setValue), | |
x.on( | |
EditableTextEvent.save, | |
EditableTextState.idle, | |
x.choose( | |
x.action('onSave'), | |
x.guard((context) => Boolean(context.value)), | |
resetValue, | |
), | |
), | |
x.on(EditableTextEvent.cancel, EditableTextState.idle, resetValue), | |
), | |
), | |
); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as yup from 'yup'; | |
import * as x from './xstate-fp'; | |
type FormElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement; | |
type ValidationSchema = yup.ObjectSchema<yup.Shape<object | undefined, Record<string, any>>>; | |
interface FormMachineContext { | |
values: Record<string, any>; | |
errors: Record<string, string> & { submit?: string }; | |
refs: Record<string, React.RefObject<FormElement>>; | |
validationSchema: ValidationSchema; | |
} | |
type FormMachineError = { | |
[inputName: string]: string; | |
}; | |
enum FormMachineEventTypes { | |
BLUR = 'BLUR', | |
CHANGE = 'CHANGE', | |
SUBMIT = 'SUBMIT', | |
} | |
const hasErrors = x.guard((context) => Object.values(context.errors).some(Boolean)); | |
const setValue = x.assign((context, event) => ({ | |
values: { | |
...context.values, | |
[event.name]: event.value, | |
}, | |
})); | |
const setErrors = x.assign({ | |
errors: (context, event) => ({ | |
...context.errors, | |
...event.data, | |
}), | |
}); | |
const resetError = x.assign({ | |
errors: (context, event) => { | |
// if the input doesnt have an error, no need to reset it | |
if (!context.errors[event.name]) return context.errors; | |
return { | |
...context.errors, | |
[event.name]: undefined, | |
}; | |
}, | |
}); | |
const resetErrors = x.assign({ errors: {} }); | |
const focusInput = x.effect((context, event): void => { | |
// this will look at the first input and focus it for some pleasant UX | |
const [firstKey] = Object.keys(event.data); | |
context.refs[firstKey as Extract<keyof typeof context.values, string>].current?.focus(); | |
}); | |
const setSubmitError = x.assign({ | |
errors: (context, event: any) => ({ | |
...context.errors, | |
submit: event.data.message, | |
}), | |
}); | |
const onChangeActions = x.composeActions(resetError, setValue); | |
const createFormMachine = () => | |
x.createMachine<FormMachineContext>( | |
x.states( | |
x.state( | |
'idle', | |
x.states(x.state('noError'), x.state('error'), x.state('submitError')), | |
x.on( | |
FormMachineEventTypes.CHANGE, | |
x.transition('idle.error', onChangeActions, hasErrors), | |
x.transition('idle.noError', onChangeActions), | |
), | |
x.on(FormMachineEventTypes.BLUR, 'validatingField'), | |
x.on(FormMachineEventTypes.SUBMIT, 'validating'), | |
), | |
x.state( | |
'validatingField', | |
x.invoke( | |
(context, event) => { | |
const field = event.name; | |
return new Promise((resolve, reject) => | |
yup | |
.reach(context.validationSchema, field) | |
.validate(event.value) | |
.then( | |
() => resolve(), | |
(error: yup.ValidationError) => reject({ [field]: error.message }), | |
), | |
); | |
}, | |
x.on('done', 'idle.noError'), | |
x.on('error', 'idle.error', setErrors), | |
), | |
), | |
x.state( | |
'validating', | |
x.entry(resetErrors), | |
x.invoke( | |
(context) => | |
new Promise((resolve, reject) => | |
// when validating the whole form, make sure we don't abort early to receive full list of errors | |
context.validationSchema | |
.validate(context.values, { | |
abortEarly: false, | |
}) | |
.then( | |
() => resolve(), | |
(error: yup.ValidationError) => reject(yupToFormErrors(error)), | |
), | |
), | |
x.on('done', 'submitting'), | |
x.on('error', 'idle.error', setErrors, focusInput), | |
), | |
), | |
x.state( | |
'submitting', | |
x.invoke( | |
'onSubmit', | |
x.on('done', 'submitted', x.action('afterSubmit')), | |
x.on('error', 'idle.submitError', setSubmitError), | |
), | |
), | |
x.final('submitted'), | |
), | |
); | |
// yup errors are kind of weird | |
// create a simple object like so { [inputName]: inputError } | |
function yupToFormErrors(yupError: yup.ValidationError): FormMachineError { | |
if (yupError.inner) { | |
if (yupError.inner.length === 0) { | |
return { [yupError.path]: yupError.message }; | |
} | |
return Object.fromEntries(yupError.inner.map((err) => [err.path, err.message])); | |
} | |
return {}; | |
} | |
export { FormMachineEventTypes, FormElement, ValidationSchema, FormMachineContext }; | |
export default createFormMachine; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as xstate from 'xstate'; | |
type StateNodeConfig< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = xstate.StateNodeConfig<TContext, TStateSchema['states'][keyof TStateSchema['states']], TEvent> & { | |
isInitial?: boolean; | |
}; | |
type StateTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = [string, StateNodeConfig<TContext, TStateSchema, TEvent>]; | |
type StatesTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = [ | |
StateNodeConfigStatesTuple<TContext, TStateSchema, TEvent>, | |
( | |
| StateNodeConfigInitialTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigTypeTuple<TContext, TStateSchema, TEvent> | |
), | |
]; | |
type StateNodeConfigInitialTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['initial', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['initial']]; | |
type StateNodeConfigTypeTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['type', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['type']]; | |
type StateNodeConfigContextTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['context', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['context']]; | |
type StateNodeConfigHistoryTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['history', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['history']]; | |
type StateNodeConfigStatesTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['states', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['states']]; | |
type StateNodeConfigInvokeTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['invoke', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['invoke']]; | |
type StateNodeConfigOnTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['on', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['on']]; | |
type StateNodeConfigEntryTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['entry', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['entry']]; | |
type StateNodeConfigExitTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['exit', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['exit']]; | |
type StateNodeConfigOnDoneTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['onDone', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['onDone']]; | |
type StateNodeConfigAfterTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['after', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['after']]; | |
type StateNodeConfigAlwaysTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['always', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['always']]; | |
type StateNodeConfigActivitiesTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['activities', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['activities']]; | |
type StateNodeConfigMetaTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['meta', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['meta']]; | |
type StateNodeConfigDataTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['data', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['data']]; | |
type StateNodeConfigIdTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = ['id', xstate.StateNodeConfig<TContext, TStateSchema, TEvent>['id']]; | |
type StateNodeConfigTuple< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
> = | |
| StateNodeConfigInitialTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigTypeTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigContextTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigHistoryTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigStatesTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigInvokeTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigOnTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigEntryTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigExitTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigOnDoneTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigAfterTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigAlwaysTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigMetaTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigDataTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigIdTuple<TContext, TStateSchema, TEvent> | |
| StateNodeConfigActivitiesTuple<TContext, TStateSchema, TEvent>; | |
type TransitionTuple<TContext = any, TEvent extends xstate.EventObject = any> = | |
| ActionTuple<TContext, TEvent> | |
| GuardActionTuple<TContext, TEvent>; | |
type ActionTuple<TContext = any, TEvent extends xstate.EventObject = any> = | |
| BaseActionTuple<TContext, TEvent> | |
| AssignActionTuple<TContext, TEvent> | |
| EffectActionTuple<TContext, TEvent> | |
| ChooseActionTuple<TContext, TEvent> | |
| ComposedActionTuple<TContext, TEvent> | |
| KeyActionTuple; | |
type ActionFunctionWithCleanup<TContext = any, TEvent extends xstate.EventObject = any> = ( | |
context: TContext, | |
event: TEvent, | |
meta: xstate.ActionMeta<TContext, TEvent>, | |
) => xstate.DisposeActivityFunction | void; | |
type KeyActionTuple = ['actions', string]; | |
type ComposedActionTuple<TContext = any, TEvent extends xstate.EventObject = any> = [ | |
'actions', | |
xstate.Action<TContext, TEvent>[], | |
]; | |
type BaseActionTuple<TContext = any, TEvent extends xstate.EventObject = any> = [ | |
'actions', | |
xstate.Action<TContext, TEvent>, | |
]; | |
type EffectActionTuple<TContext = any, TEvent extends xstate.EventObject = any> = [ | |
'actions', | |
xstate.ActionFunction<TContext, TEvent> | ActionFunctionWithCleanup<TContext, TEvent>, | |
]; | |
type AssignActionTuple<TContext = any, TEvent extends xstate.EventObject = any> = [ | |
'actions', | |
xstate.AssignAction<TContext, TEvent>, | |
]; | |
type GuardActionTuple<TContext = any, TEvent extends xstate.EventObject = any> = [ | |
'cond', | |
xstate.Condition<TContext, TEvent>, | |
]; | |
type ChooseActionTuple<TContext = any, TEvent extends xstate.EventObject = any> = [ | |
'actions', | |
xstate.ChooseAction<TContext, TEvent>, | |
]; | |
type Delay<TContext = any, TEvent extends xstate.EventObject = any> = | |
| number | |
| ((context: TContext, event: TEvent) => number); | |
type DelayedTransitionTuple<TContext = any, TEvent extends xstate.EventObject = any> = [ | |
Delay<TContext, TEvent>, | |
xstate.TransitionConfig<TContext, TEvent>[], | |
]; | |
function last<T>(array: T[]): T { | |
return array[array.length - 1]; | |
} | |
function extractStates< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>(args: (StateNodeConfigTuple<TContext, TStateSchema, TEvent> | StatesTuple<TContext, TStateSchema, TEvent>)[]) { | |
const states = args.find(([maybeArray]) => Array.isArray(maybeArray)) as | |
| StatesTuple<TContext, TStateSchema, TEvent> | |
| undefined; | |
return [...args.filter(([maybeArray]) => !Array.isArray(maybeArray)), ...(states ?? [])]; | |
} | |
function extractConfig< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>( | |
args: (StateNodeConfigTuple<TContext, TStateSchema, TEvent> | StatesTuple<TContext, TStateSchema, TEvent>)[], | |
): xstate.StateNodeConfig<TContext, TStateSchema, TEvent> { | |
const nextArgs = extractEvents(extractStates(args)); | |
return Object.fromEntries(nextArgs); | |
} | |
function extractEvents< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>( | |
args: (StateNodeConfigTuple<TContext, TStateSchema, TEvent> | StatesTuple<TContext, TStateSchema, TEvent>)[], | |
): (StateNodeConfigTuple<TContext, TStateSchema, TEvent> | StatesTuple<TContext, TStateSchema, TEvent>)[] { | |
const events = args.filter(([key]) => key === 'on') as StateNodeConfigOnTuple<TContext, TStateSchema, TEvent>[]; | |
const nextArgs = args.filter(([key]) => key !== 'on'); | |
if (events.length) { | |
const { done, ...reducedEvents } = events.reduce( | |
(events, [_key, event]) => ({ ...events, ...event }), | |
{} as Record<string, xstate.TransitionConfig<TContext, TEvent>[]>, | |
); | |
if (done) { | |
// we destructure done because it's a reserved event | |
nextArgs.push(['onDone', done] as StateNodeConfigOnDoneTuple<TContext, TStateSchema, TEvent>); | |
} | |
nextArgs.push(['on', reducedEvents as any]); | |
} | |
return nextArgs; | |
} | |
function extractActions<TContext = any, TEvent extends xstate.EventObject = any>( | |
args: TransitionTuple<TContext, TEvent>[] | (string | TransitionTuple<TContext, TEvent>)[], | |
) { | |
return args | |
.filter(([key]) => key === 'actions') | |
.map(([_key, actions]) => actions) | |
.flat() as xstate.Action<TContext, TEvent>[]; | |
} | |
function extractGuards<TContext = any, TEvent extends xstate.EventObject = any>( | |
args: TransitionTuple<TContext, TEvent>[] | (string | TransitionTuple<TContext, TEvent>)[], | |
) { | |
return args.filter(([key]) => key === 'cond').map(([_key, guards]) => guards); | |
} | |
function extractTransitions<TContext = any, TEvent extends xstate.EventObject = any>( | |
args: (string | xstate.TransitionConfig<TContext, TEvent> | TransitionTuple<TContext, TEvent>)[], | |
): xstate.TransitionConfig<TContext, TEvent>[] { | |
return args.reduce((transitions, maybeTransitionTuple) => { | |
if (typeof maybeTransitionTuple === 'object' && !Array.isArray(maybeTransitionTuple)) { | |
// if its an object, then its a transition object made from the transition function | |
transitions.push(maybeTransitionTuple); | |
return transitions; | |
} | |
let currentTransition = last(transitions); | |
// if the currentTransition has cond defined, the next args will describe a new transition | |
// until the next the end or until another cond is defined | |
if (!currentTransition || currentTransition.cond) { | |
currentTransition = {}; | |
transitions.push(currentTransition); | |
} | |
if (typeof maybeTransitionTuple === 'string') { | |
// if it's a string, then we'll treat it as a transition target | |
currentTransition.target = maybeTransitionTuple; | |
} | |
if (Array.isArray(maybeTransitionTuple)) { | |
// if its an array then it's a tuple | |
const [type, config] = maybeTransitionTuple; | |
if (type === 'actions') { | |
const actions = currentTransition.actions | |
? ([...currentTransition.actions, config] as xstate.Action<TContext, TEvent>[]) | |
: ([config] as xstate.Action<TContext, TEvent>[]); | |
currentTransition.actions = actions; | |
} | |
if (type === 'cond') { | |
currentTransition.cond = config as xstate.Guard<TContext, TEvent>; | |
} | |
} | |
return transitions; | |
}, [] as xstate.TransitionConfig<TContext, TEvent>[]); | |
} | |
export function states< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>(...args: StateTuple<TContext, TStateSchema, TEvent>[]): StatesTuple<TContext, TStateSchema, TEvent> { | |
const maybeInitialStateTuple = args.find(([_key, stateConfig]) => stateConfig.isInitial); | |
let initialTuple: ['initial', keyof TStateSchema['states']]; | |
if (maybeInitialStateTuple) { | |
const [stateName, config] = maybeInitialStateTuple; | |
initialTuple = ['initial', stateName as keyof TStateSchema['states']]; | |
delete config.isInitial; | |
} else { | |
const [[firstStateKey]] = args; | |
initialTuple = ['initial', firstStateKey as keyof TStateSchema['states']]; | |
} | |
const states = Object.fromEntries(args) as xstate.StatesConfig<TContext, TStateSchema, TEvent>; | |
return [['states', states], initialTuple]; | |
} | |
export function parallel< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>( | |
...args: [string, xstate.StateNodeConfig<TContext, TStateSchema, TEvent>][] | |
): StatesTuple<TContext, TStateSchema, TEvent> { | |
const states = Object.fromEntries(args) as xstate.StatesConfig<TContext, TStateSchema, TEvent>; | |
return [ | |
['states', states], | |
['type', 'parallel'], | |
]; | |
} | |
export function initial< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>( | |
stateName: string, | |
...args: (StateNodeConfigTuple<TContext, TStateSchema, TEvent> | StatesTuple<TContext, TStateSchema, TEvent>)[] | |
): StateTuple<TContext, TStateSchema, TEvent> { | |
const [, stateConfig] = state<TContext, TStateSchema, TEvent>(stateName, ...args); | |
return [stateName, { ...stateConfig, isInitial: true }]; | |
} | |
export function state< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>( | |
stateName: string, | |
...args: (StateNodeConfigTuple<TContext, TStateSchema, TEvent> | StatesTuple<TContext, TStateSchema, TEvent>)[] | |
): StateTuple<TContext, TStateSchema, TEvent> { | |
return [ | |
stateName, | |
extractConfig<TContext, TStateSchema, TEvent>(args) as xstate.StateNodeConfig< | |
TContext, | |
TStateSchema['states'][keyof TStateSchema['states']], | |
TEvent | |
>, | |
]; | |
} | |
export function final<TContext = any, TEvent extends xstate.EventObject = any>( | |
stateName: string, | |
): [string, xstate.FinalStateNodeConfig<TContext, TEvent>] { | |
return [stateName, { type: 'final' }]; | |
} | |
export function on< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>( | |
event: string, | |
...args: (string | xstate.TransitionConfig<TContext, TEvent> | TransitionTuple<TContext, TEvent>)[] | |
): StateNodeConfigOnTuple<TContext, TStateSchema, TEvent> { | |
const eventTuple = [event, extractTransitions<TContext, TEvent>(args)]; | |
return ['on', Object.fromEntries([eventTuple])]; | |
} | |
export function assign<TContext = any, TEvent extends xstate.EventObject = any>( | |
assignment: xstate.Assigner<TContext, TEvent> | xstate.PropertyAssigner<TContext, TEvent>, | |
): AssignActionTuple<TContext, TEvent> { | |
return ['actions', xstate.assign<TContext, TEvent>(assignment)]; | |
} | |
export function action(act: string): KeyActionTuple { | |
return ['actions', act]; | |
} | |
export function effect<TContext = any, TEvent extends xstate.EventObject = any>( | |
effect: xstate.ActionFunction<TContext, TEvent> | ActionFunctionWithCleanup<TContext, TEvent>, | |
): EffectActionTuple<TContext, TEvent> { | |
return ['actions', effect]; | |
} | |
export function guard<TContext = any, TEvent extends xstate.EventObject = any>( | |
cond: xstate.Condition<TContext, TEvent>, | |
): GuardActionTuple<TContext, TEvent> { | |
return ['cond', cond]; | |
} | |
export function transition<TContext = any, TEvent extends xstate.EventObject = any>( | |
...args: | |
| [string, ...TransitionTuple<TContext, TEvent>[]] | |
| [TransitionTuple<TContext, TEvent>, ...TransitionTuple<TContext, TEvent>[]] | |
): xstate.TransitionConfig<TContext, TEvent> { | |
const actions = extractActions<TContext, TEvent>(args); | |
const cond = last(extractGuards<TContext, TEvent>(args)) as xstate.Condition<TContext, TEvent> | undefined; | |
return { | |
target: args.find((arg) => typeof arg === 'string') as string, | |
actions: actions.length ? actions : undefined, | |
cond, | |
}; | |
} | |
export function invoke< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>( | |
src: xstate.InvokeConfig<TContext, TEvent>['src'], | |
...args: StateNodeConfigOnTuple[] | |
): StateNodeConfigInvokeTuple<TContext, TStateSchema, TEvent> { | |
const on = args | |
.filter(([key]) => key === 'on') | |
.reduce( | |
(events, [_key, event]) => ({ ...events, ...event }), | |
{} as { | |
done: xstate.TransitionConfig<TContext, xstate.DoneInvokeEvent<any>>[]; | |
error: xstate.TransitionConfig<TContext, xstate.DoneInvokeEvent<any>>[]; | |
}, | |
); | |
return [ | |
'invoke', | |
{ | |
src, | |
onDone: on.done, | |
onError: on.error, | |
}, | |
]; | |
} | |
export function always< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>(...args: TransitionTuple<TContext, TEvent>[]): StateNodeConfigAlwaysTuple<TContext, TStateSchema, TEvent> { | |
return ['always', extractTransitions<TContext, TEvent>(args)]; | |
} | |
export function choose<TContext = any, TEvent extends xstate.EventObject = any>( | |
...choices: TransitionTuple<TContext, TEvent>[] | |
): ChooseActionTuple<TContext, TEvent> { | |
return [ | |
'actions', | |
xstate.actions.choose<TContext, TEvent>( | |
// we can leverage the extractTransitions function because we disallow strings at the type level | |
extractTransitions<TContext, TEvent>(choices) as xstate.ChooseConditon<TContext, TEvent>[], | |
), | |
]; | |
} | |
export function choice<TContext = any, TEvent extends xstate.EventObject = any>( | |
...args: [ActionTuple<TContext, TEvent>, ...TransitionTuple<TContext, TEvent>[]] | [ActionTuple<TContext, TEvent>] | |
): xstate.ChooseConditon<TContext, TEvent> { | |
const actions = extractActions<TContext, TEvent>(args); | |
const cond = last(extractGuards<TContext, TEvent>(args)) as xstate.Condition<TContext, TEvent> | undefined; | |
return { | |
cond, | |
actions, | |
}; | |
} | |
export function id< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>(name: string): StateNodeConfigIdTuple<TContext, TStateSchema, TEvent> { | |
return ['id', name]; | |
} | |
export function entry< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>(...actions: BaseActionTuple<TContext, TEvent>[]): StateNodeConfigEntryTuple<TContext, TStateSchema, TEvent> { | |
return ['entry', extractActions<TContext, TEvent>(actions)]; | |
} | |
export function exit< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>(...actions: BaseActionTuple<TContext, TEvent>[]): StateNodeConfigExitTuple<TContext, TStateSchema, TEvent> { | |
return ['exit', extractActions<TContext, TEvent>(actions)]; | |
} | |
export function meta< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>( | |
meta: TStateSchema extends { | |
meta: infer D; | |
} | |
? D | |
: any, | |
): StateNodeConfigMetaTuple<TContext, TStateSchema, TEvent> { | |
return ['meta', meta]; | |
} | |
export function data< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>( | |
data: xstate.Mapper<TContext, TEvent, any> | xstate.PropertyMapper<TContext, TEvent, any>, | |
): StateNodeConfigDataTuple<TContext, TStateSchema, TEvent> { | |
return ['data', data]; | |
} | |
export function history< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>(history: 'none' | 'shallow' | 'deep'): StateNodeConfigHistoryTuple<TContext, TStateSchema, TEvent> { | |
return ['history', history === 'none' ? false : history]; | |
} | |
export function context< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>(context: TContext | (() => TContext)): StateNodeConfigTuple<TContext, TStateSchema, TEvent> { | |
return ['context', context]; | |
} | |
export function activities< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>(...args: EffectActionTuple<TContext, TEvent>[]): StateNodeConfigActivitiesTuple<TContext, TStateSchema, TEvent> { | |
const activities: xstate.ActivityDefinition<TContext, TEvent>[] = args.map(([, activity]) => { | |
const name = activity.name || activity.toString(); | |
return { | |
id: name, | |
type: name, | |
exec: activity, | |
}; | |
}); | |
return ['activities', activities]; | |
} | |
export function after<TContext = any, TEvent extends xstate.EventObject = any>( | |
...args: DelayedTransitionTuple<TContext, TEvent>[] | |
) { | |
return ['after', Object.fromEntries(args)]; | |
} | |
export function delay<TContext = any, TEvent extends xstate.EventObject = any>( | |
delay: Delay<TContext, TEvent>, | |
...args: (string | xstate.TransitionConfig<TContext, TEvent> | TransitionTuple<TContext, TEvent>)[] | |
): DelayedTransitionTuple<TContext, TEvent> { | |
return [delay, extractTransitions<TContext, TEvent>(args)]; | |
} | |
export function composeActions<TContext = any, TEvent extends xstate.EventObject = any>( | |
...args: ActionTuple<TContext, TEvent>[] | |
): ActionTuple<TContext, TEvent> { | |
const actions = extractActions<TContext, TEvent>(args); | |
return ['actions', actions]; | |
} | |
export function createMachine< | |
TContext = any, | |
TStateSchema extends xstate.StateSchema<any> = any, | |
TEvent extends xstate.EventObject = any | |
>(...args: (StateNodeConfigTuple<TContext, TStateSchema, TEvent> | StatesTuple<TContext, TStateSchema, TEvent>)[]) { | |
const config = extractConfig<TContext, TStateSchema, TEvent>(args) as xstate.MachineConfig< | |
TContext, | |
TStateSchema, | |
TEvent | |
>; | |
return xstate.Machine<TContext, TStateSchema, TEvent>(config); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Awesome! Hope that your code can be a package!