Skip to content

Releases: LevelbossMike/ember-statecharts

v0.16.1

10 Feb 10:28
Compare
Choose a tag to compare

0.16.1

Patch Changes

v0.16.0

08 Feb 11:30
Compare
Choose a tag to compare

0.16.0

Minor Changes

  • 9ee0ef3: #393 Allow passing interpreterOptions to useMachine - LevelbossMike

    🥁 Features

    InterpreterOptions

    Bring back ability to customize InterpreterOptions that are used when a machine passed to useMachine gets interpreted. This is probably most useful for people that want to have xState delays etc. be scheduled via a custom clock service that uses Ember's runloop, but can be used to pass other interpreterOptions as well.

    To customize interpreterOptions, when you would like to provide the same options every time you use useMachine, you can create a wrapper-function for useMachine:

    Example:

    const wrappedUseMachine = (context, options) => {
      return useMachine(context, () => {
        return {
          interpreterOptions: {
            clock: {
              setTimeout(fn, timeout) {
                later.call(null, fn, timeout);
              },
              clearTimeout(id) {
                cancel.call(null, id);
              },
            },
          },
          ...options(),
        };
      });
    };
    
    statechart = wrappedUseMachine(this, () => {
      // ...
    });

    runloopClockService-configuration option

    For the use-case of having xState timeouts etc. be schedule via the runloop, ember-statecharts now provides a configuration option. You can turn it on to have xState use a custom clock and schedule, and cancel timeouts via the ember-runloop. This makes testing via @ember/test-helpers arguably more ergonomic, as you won't need to explicitly await ui changes that are triggered by schedule statechart changes explicitly anymore.

    Example - based on the test that tests this behavior:

    import { module, test } from 'qunit';
    import { setupRenderingTest } from 'test-app/tests/helpers';
    import { click, render, waitUntil } from '@ember/test-helpers';
    import { hbs } from 'ember-cli-htmlbars';
    
    module('Integration | Component | use-machine', function (hooks) {
      setupRenderingTest(hooks);
    
      test('awaiting needs to be explicit with no `runloopClockService`-option set', async function (assert) {
        const config = this.owner.resolveRegistration('config:environment');
    
        config['ember-statecharts'] = {
          runloopClockService: false,
        };
    
        await render(hbs`<DelayedToggle data-test-toggle />`);
    
        assert.dom('[data-test-off]').exists('initially toggle is off');
    
        await click('[data-test-toggle]');
    
        // oh noes, we need to wait explicitly as xState uses `window.setTimeout` by default 😢
        await waitUntil(() => {
          return document.querySelector('[data-test-off]');
        });
    
        assert.dom('[data-test-off]').exists('initially toggle is off');
      });
    
      test('awaiting happens automatically when `runloopClockService`-option is set', async function (assert) {
        const config = this.owner.resolveRegistration('config:environment');
    
        config['ember-statecharts'] = {
          runloopClockService: true,
        };
    
        await render(hbs`<DelayedToggle data-test-toggle />`);
    
        assert.dom('[data-test-off]').exists('initially toggle is off');
    
        // just awaiting the click is enough now, because we schedule delays on the runloop 🥳
        await click('[data-test-toggle]');
    
        assert.dom('[data-test-off]').exists('initially toggle is off');
      });
    });

    To turn on this behavior, you can set the runloopClockService-configuration in config/environment.js:

    ENV['ember-statecharts'] = {
      runloopClockService: true,
    };

v0.15.1

30 Nov 14:42
Compare
Choose a tag to compare

0.15.1

Patch Changes

v0.15.0

24 Nov 14:51
Compare
Choose a tag to compare

0.15.0

Minor Changes

  • 1c27cb6: Port add-on to a proper v2 add-on setup.

    This restructuring does change the behavior of the add-on slightly and is introducing breaking changes.

    🥁 Features

    use-machine is now an ember-resource-resource

    ember-statecharts now uses ember-resources under the hood to provide its functionality. Make sure you install it as a dependency of your project:

    ember install ember-resources

    This doesn't change the way you use useMachine but comes with the additional benefit of you now having the possibilty to create resources in your application and model their behavior explicitly via useMachine:

    Example:

    import { tracked } from '@glimmer/tracking';
    import { Resource } from 'ember-resources';
    import { useMachine } from 'ember-statecharts';
    import asyncMachine from '../machines/async';
    
    function noop = async function() {}
    
    class Async extends Resource {
      @tracked onSuccess;
      @tracked onError;
      @tracked onSubmit;
    
      statechart = useMachine(this, () => {
        return {
          machine: asyncMachine.withConfig({
            actions: {
              onError,
              onSuccess,
            },
            services: {
              onSubmit
            }
          })
        }
      });
    
      submit = () => {
        this.statechart.send('SUBMIT');
      }
    
      retry = () => {
        this.statechart.send('RETRY');
      }
    
      get isBusy() {
        return this.statechart.state.matches('busy');
      }
    
      modify(positional, { onSubmit, onError, onSuccess}) {
        this.onSubmit = onSubmit || noop;
        this.onError = onError || noop;
        this.onSuccess = onSuccess || noop;
      }
    }
    
    // use it somewhere
    import Async from '../resources/async';
    
    class AsyncButton extends Component {
      async = Async.from(this, () => {
        return {
          onSubmit,
          onSuccess,
          onError
        }
      })
    
      get isDisabled() {
        return this.async.isBusy;
      }
    
      // ...
    }

    Please refer to the documentation of ember-resources to learn more how to implement your own custom resources, and how to use them in your projects.

    💥 Breaking changes

    statechart-computed-macro has been removed

    The @statechart-macro, i.e. the computed based statechart API has been removed from the add-on completely. ember-statecharts is a proper v2-addon now and chances
    are that if you still rely on the statechart-computed you won't need to update to a v2-addon. The API also does
    not play very well with the Octane paradigms.

    @matchesState-decorator removed

    The @matchesState-decorator has been removed. Decorators don't play very well with TypeScript and matching against state is a built-in feature of XState.

    Refactoring your @matchesState-decorators towards reactive getters is rather simple:

    @matchesState({ interactivity: 'idle'})
    isIdle;
    
    // => refactor towards native getter
    
    get isIdle() {
      return this.statechart.state.matches({ interactivity: 'idle' })
    }

    If you can't live without the decorator it is also rather simple to recreate it in your own project:

    import { matchesState as xStateMatchesState } from 'xstate';
    
    function matchesState(
      state: StateValue,
      statechartPropertyName = 'statechart'
    ): any {
      return function () {
        return {
          get(this: any): boolean {
            const statechart = this[statechartPropertyName] as
              | { state: { value: StateValue } }
              | undefined;
    
            if (statechart) {
              return xstateMatchesState(state, statechart.state.value);
            } else {
              return false;
            }
          },
        };
      };
    }
    
    // you can use this as a @matchesState-decorator
    class ButtonComponent extends Component {
      statechart = useMachine(this, () => {
        // ...
      });
    
      @matchesState('busy') isBusy;
    }

Release 0.15.0-beta.0

11 May 15:08
Compare
Choose a tag to compare
Release 0.15.0-beta.0 Pre-release
Pre-release

0.15.0-beta.0 (2022-05-11)

This beta makes ember-statecharts compatible with embroider. You have to use ember-auto-import-v2 to use this release.

Bug Fixes

  • make linter happy (af41285)
  • make sure addon works with embroider (3178b07)
  • more fixes for embroider (4ddc193)
  • pin eslint to 7.29 (cc327c5)
  • remove ember-auto-import from dev dependencies (0b6a4f7)
  • test-setup test-app / site (15d29e9)

Release 0.14.0

09 Mar 19:15
Compare
Choose a tag to compare

0.14.0 (2022-03-09)

Bug Fixes

Features

v0.14.0-beta.0

03 Dec 17:01
Compare
Choose a tag to compare
v0.14.0-beta.0 Pre-release
Pre-release

This is the first prerelease of the new updated useMachine-API that does not require ember-usable anymore but is implemented based on the `invokeHelper-API. You need to run Ember.js >= 3.24.x to use this release.

Changes in this release:

new useMachine-API

useMachine is now a helper. Thus the invocation has changed slightly:

import { useMachine, matchesState } from 'ember-statecharts';

export default class ToggleComponent extends Component {
  statechart = useMachine(this, () => {
    return {
      machine: toggleMachine.withConfig({ /* ... */ }).withContext({ /* ... */}),
      update({ machine, restart, send }) => { /* ... */}, // optional
      onTransition(state) => { /* ... */}, // optional
      initialState: this.args.state, // optional
      interpreterOptions: { /* ... */} // optional
    };
  });

  @matchesState('off') isOff;
  @matchesState('on') isOn;

  @action toggle() {
    this.statechart.send('TOGGLE');
  }
}

XState is a peer dependency now

XState now needs to be installed separately - ember-statecharts will not pull in this dependency for you anymore:


yarn add -D xstate

or

npm install --save-dev xstate

No more ember-usable

This addon does not depend on a separate useable-addon anymore. If your app relies on the previous implicit install of ember-usable you will need to install it yourself now.

Release 0.13.2

25 Sep 14:59
Compare
Choose a tag to compare

0.13.2 (2020-09-25)

Release 0.13.1

07 Sep 11:49
Compare
Choose a tag to compare

0.13.1 (2020-09-07)

Updated docs page - no code updates

17c7003

Release 0.13.0

06 Sep 17:55
Compare
Choose a tag to compare

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