Skip to content

Commit

Permalink
docs: update docs with TypeScript section
Browse files Browse the repository at this point in the history
  • Loading branch information
LevelbossMike committed Sep 6, 2020
1 parent 2ab8f21 commit 238b952
Show file tree
Hide file tree
Showing 6 changed files with 325 additions and 0 deletions.
44 changes: 44 additions & 0 deletions tests/dummy/app/components/docs/typescript-usage.hbs
@@ -0,0 +1,44 @@
<DocsDemo as |demo|>
<demo.example @name="final">
<div class="docs-flex docs-item-center">
{{!-- BEGIN-SNIPPET typed-button-used.hbs --}}
<TypedButton
@onClick={{perform this.submitTask}}
@onSuccess={{this.onSuccess}}
@onError={{this.onError}}
@disabled={{this.disabled}}
>
.ts FTW
</TypedButton>
{{!-- END-SNIPPET --}}
<div class="docs-ml-8 docs-flex docs-items-center">
<label for="failRequest-final">
Fail request?
</label>
<Input
@class="docs-ml-2"
@type="checkbox"
@id="failRequest-final"
@name="failRequest"
@checked={{this.failRequest}}
/>
</div>
<div class="docs-ml-8 docs-flex docs-items-center">
<label for="disabled-final">
Disable button?
</label>
<Input
@class="docs-ml-2"
@type="checkbox"
@id="disabled-final"
@name="failRequest"
@checked={{this.disabled}}
/>
</div>
</div>
</demo.example>
<demo.snippet @name="typed-button.ts" @label="components.ts" />
<demo.snippet @name="typed-button-machine.ts" @label="machine.ts" />
<demo.snippet @name="typed-button-used.hbs" label="template.hbs" />
<demo.snippet @name="typescript-usage.js" label="controller.js" />
</DocsDemo>
37 changes: 37 additions & 0 deletions tests/dummy/app/components/docs/typescript-usage.js
@@ -0,0 +1,37 @@
import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { task, timeout } from 'ember-concurrency';

export default class extends Component {
@tracked
failRequest = false;

@tracked
disabled = false;

// BEGIN-SNIPPET typescript-usage.js

// ...

@(task(function* () {
yield timeout(1000);

if (this.failRequest) {
throw 'wat';
}
}).drop())
submitTask;

@action
onSuccess() {
window.alert('Submit successful');
}

@action
onError() {
window.alert('Submit failed');
}
// ...
// END-SNIPPET
}
19 changes: 19 additions & 0 deletions tests/dummy/app/components/typed-button.hbs
@@ -0,0 +1,19 @@
{{!-- BEGIN-SNIPPET typed-button.hbs --}}
<button
type="button"
class="
docs-w-32 docs-border docs-p-4 docs-bg-brand docs-border-brand docs-shadow
docs-rounded docs-outline-none docs-text-white docs-font-bold
{{if this.showAsDisabled "docs-cursor-not-allowed docs-opacity-50"}}
"
{{on "click" this.handleClick}}
disabled={{this.isDisabled}}
...attributes
>
{{#if this.isBusy}}
<UiLoading data-test-loading />
{{else}}
{{yield}}
{{/if}}
</button>
{{!-- END-SNIPPET --}}
101 changes: 101 additions & 0 deletions tests/dummy/app/components/typed-button.ts
@@ -0,0 +1,101 @@
// BEGIN-SNIPPET typed-button.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
import Component from '@glimmer/component';
import { useMachine, matchesState, interpreterFor } from 'ember-statecharts';
import buttonMachine, { ButtonContext, ButtonEvent, ButtonState } from '../machines/typed-button';
import { TaskGenerator } from 'ember-concurrency';

import { task } from 'ember-concurrency-decorators';
import { taskFor } from 'ember-concurrency-ts';

import { use } from 'ember-usable';
import { action } from '@ember/object';

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> {
get onClick(): any {
return this.args.onClick || noop;
}

@matchesState({ activity: 'busy' })
isBusy!: boolean;

@matchesState({ interactivity: 'disabled' })
isDisabled!: boolean;

@matchesState({ interactivity: 'unknown' })
isInteractivityUnknown!: boolean;

get showAsDisabled(): boolean {
const { isDisabled, isBusy, isInteractivityUnknown } = this;

return isDisabled || isBusy || isInteractivityUnknown;
}

@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 {
const interpreter = interpreterFor(this.statechart);

interpreter.send('SUBMIT');
}

@action
onSuccess(_context: ButtonContext, { result }: Extract<ButtonEvent, { type: 'SUCCESS ' }>): any {
const functionToCall = this.args.onSuccess || noop;

return functionToCall(result);
}

@action
onError(_context: ButtonContext, { error }: Extract<ButtonEvent, { type: 'ERROR' }>): any {
const functionToCall = this.args.onError || noop;

return functionToCall(error);
}

@action
performSubmitTask(): void {
taskFor(this.submitTask).perform();
}
}
// END-SNIPPET
99 changes: 99 additions & 0 deletions tests/dummy/app/machines/typed-button.ts
@@ -0,0 +1,99 @@
/* eslint-disable @typescript-eslint/no-empty-function */
// BEGIN-SNIPPET typed-button-machine.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;
},
},
}
);
// END-SNIPPET
25 changes: 25 additions & 0 deletions tests/dummy/app/templates/docs/statecharts.md
Expand Up @@ -652,6 +652,31 @@ be used as the primary way to trigger side-effects based on state transitions -
you want to use [actions](https://xstate.js.org/docs/guides/actions.html) for
that instead.

## Working with TypeScript

`ember-statecharts` itself is implemented in TypeScript and fully supports
Ember.js apps that are written in TypeScript. Due to the way xstate works
internally it is rather verbose to type your machines but as always with
TypeScript you will end up with better developer ergonomics than you would when
not typing your code.

The `useMachine` api supports both versions of typing machines:

1. Without typestates: `useMachine<TContext, TStateSchema, TEvent>(/* ... */)`
2. With typestates: `useMachine<TContext, any, TEvent, TTypestate>(/* ... */)`

Please refer to the [using TypeScript](https://xstate.js.org/docs/guides/typescript.html#using-typescript) of the xstate docs for a thorough walkthrough on how to type your xstate machines.

Like [ember-concurrency](https://jamescdavis.com/using-ember-concurrency-with-typescript/) `ember-statecharts` has to use a typecasting function to allow TypeScript understand what `useMachine` is trying to accomplish. Whenever you want to interact with the usable you have to wrap your statechart property in `interpreterFor`.

`interpreterFor` is not doing anything special but only typecasting the usable
so that TypeScript can provide useful type information.

Next up you see an example of the `Button`-component from the tutorial
implemented in TypeScript:

<Docs::TypescriptUsage />

## Legacy api

`ember-statecharts` still ships with the legacy `computed`-macro api. If you
Expand Down

0 comments on commit 238b952

Please sign in to comment.