Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert useMachine to TypeScript #300

Merged
merged 9 commits into from Sep 6, 2020
Merged

Convert useMachine to TypeScript #300

merged 9 commits into from Sep 6, 2020

Conversation

LevelbossMike
Copy link
Owner

@LevelbossMike LevelbossMike commented Sep 2, 2020

This PR 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 enabilng 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.

This PR also adds a section to the docs that discusses how to use TypeScript with ember-statecharts.

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

@LevelbossMike LevelbossMike marked this pull request as draft September 2, 2020 15:05
@LevelbossMike LevelbossMike force-pushed the MIKE/typescript branch 3 times, most recently from 528f712 to 3043d61 Compare September 4, 2020 10:01
@LevelbossMike LevelbossMike marked this pull request as ready for review September 4, 2020 10:17
@LevelbossMike LevelbossMike changed the title Mike/typescript TypeScript support for useMachine Sep 4, 2020
@LevelbossMike LevelbossMike changed the title TypeScript support for useMachine Convert useMachine to TypeScript Sep 4, 2020
@knownasilya
Copy link

Nice!

Copy link
Collaborator

@pangratz pangratz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BEAUTIFUL

@pangratz
Copy link
Collaborator

pangratz commented Sep 6, 2020

Thanks for your hard work on this!

@LevelbossMike LevelbossMike merged commit 238b952 into master Sep 6, 2020
@LevelbossMike LevelbossMike deleted the MIKE/typescript branch September 6, 2020 17:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants