Releases: LevelbossMike/ember-statecharts
v0.16.1
v0.16.0
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 useuseMachine
, you can create a wrapper-function foruseMachine
: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 optionFor 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 inconfig/environment.js
:ENV['ember-statecharts'] = { runloopClockService: true, };
v0.15.1
0.15.1
Patch Changes
-
da688ce: #392 Avoid changing state property - miguelcobain
Make sure to only update the resources
state
-property when the state actually changed. This makes sure
we don't trigger reactive getters unnecessarily.See Xstate-Transitions docs for reference.
v0.15.0
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 anember-resource
-resourceember-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 viauseMachine
: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 removedThe
@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 thestatechart
-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 removedThe
@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
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
Release 0.14.0
0.14.0 (2022-03-09)
Bug Fixes
- site: downgrade ember-showcase to 0.1.0 (1e057ef)
- dedupe ember-cli-babel (12f2063)
- linting errors after ember upgrade (ff6c7a7)
- make sure to use any port available for testing (9d93f1e)
- onEntry -> entry; onExit -> exit (d4ecd5b)
- override docs-viewer (b19101b)
- try to fix CI Build with auto-import-v2 (80b409f), closes /github.com/emberjs/ember.js/pull/19761#issuecomment-942623604
- use custom docs-header (154850f)
Features
v0.14.0-beta.0
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 use
able-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
0.13.2 (2020-09-25)
Release 0.13.1
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();
}
}