Release 0.13.0
0.13.0 (2020-09-06)
Features
- use TypeScript for
use-machine
(1b1fdae)
This release refactors the useMachine
-api to TypeScript and introduces the interpreterFor
-typecasting function that allows TypeScript to do meaningful typechecking on useMachine
.
The idea for interpreterFor
is inspired by how ember-concurrency
is dealing with enabling TypeScript support for their apis - see https://jamescdavis.com/using-ember-concurrency-with-typescript/ and https://github.com/chancancode/ember-concurrency-ts for details about why this is necessary and what ember-concurrency
is doing to allow proper typechecking.
In short interpreterFor
doesn't change the code but typecasts the useMachine
-usable so that TypeScript understands that we are not dealing with the ConfigurableMachineDefinition
anymore but an InterpreterUsable
that you can send events to.
Checkout documentation about this new feature in the docs: https://ember-statecharts.com/docs/statecharts#working-with-typescript
Here's a code example of how usage of ember-statecharts
will look like with TypeScript - I added the respective machine definition behind a collapsable code block for readability.
/app/machines/typed-button.ts - (Machine-Definition)
// app/machines/typed-button.ts
import { createMachine } from 'xstate';
export interface ButtonContext {
disabled?: boolean;
}
export type ButtonEvent =
| { type: 'SUBMIT' }
| { type: 'SUCCESS'; result: any }
| { type: 'ERROR'; error: any }
| { type: 'ENABLE' }
| { type: 'DISABLE' };
export type ButtonState =
| { value: 'idle'; context: { disabled?: boolean } }
| { value: 'busy'; context: { disabled?: boolean } }
| { value: 'success'; context: { disabled?: boolean } }
| { value: 'error'; context: { disabled?: boolean } };
export default createMachine<ButtonContext, ButtonEvent, ButtonState>(
{
type: 'parallel',
states: {
interactivity: {
initial: 'unknown',
states: {
unknown: {
on: {
'': [{ target: 'enabled', cond: 'isEnabled' }, { target: 'disabled' }],
},
},
enabled: {
on: {
DISABLE: 'disabled',
},
},
disabled: {
on: {
ENABLE: 'enabled',
},
},
},
},
activity: {
initial: 'idle',
states: {
idle: {
on: {
SUBMIT: {
target: 'busy',
cond: 'isEnabled',
},
},
},
busy: {
entry: ['handleSubmit'],
on: {
SUCCESS: 'success',
ERROR: 'error',
},
},
success: {
entry: ['handleSuccess'],
on: {
SUBMIT: {
target: 'busy',
cond: 'isEnabled',
},
},
},
error: {
entry: ['handleError'],
on: {
SUBMIT: {
target: 'busy',
cond: 'isEnabled',
},
},
},
},
},
},
},
{
actions: {
handleSubmit() {},
handleSuccess() {},
handleError() {},
},
guards: {
isEnabled(context) {
return !context.disabled;
},
},
}
);
// app/components/typed-button.ts
// ...
import { useMachine, matchesState, interpreterFor } from 'ember-statecharts';
import buttonMachine, { ButtonContext, ButtonEvent, ButtonState } from '../machines/typed-button';
interface ButtonArgs {
disabled?: boolean;
onClick?: () => any;
onSuccess?: (result: any) => any;
onError?: (error: any) => any;
}
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
function noop() {}
export default class TypedButton extends Component<ButtonArgs> {
// ...
@use statechart = useMachine<ButtonContext, any, ButtonEvent, ButtonState>(buttonMachine)
.withContext({
disabled: this.args.disabled,
})
.withConfig({
actions: {
handleSubmit: this.performSubmitTask,
handleSuccess: this.onSuccess,
handleError: this.onError,
},
})
.update(({ context, send }) => {
const disabled = context?.disabled;
if (disabled) {
send('DISABLE');
} else {
send('ENABLE');
}
});
@task *submitTask(): TaskGenerator<void> {
try {
const result = yield this.onClick();
interpreterFor(this.statechart).send('SUCCESS', { result });
} catch (e) {
interpreterFor(this.statechart).send('ERROR', { error: e });
}
}
@action
handleClick(): void {
interpreterFor(this.statechart).send('SUBMIT');
}
// ...
@action
performSubmitTask(): void {
taskFor(this.submitTask).perform();
}
}