Skip to content

Release 0.13.0

Compare
Choose a tag to compare
@LevelbossMike LevelbossMike released this 06 Sep 17:55
· 169 commits to master since this release

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();
  }
}