├── ember-statechart-component ├── .eslintignore ├── .gitignore ├── babel.config.json ├── .editorconfig ├── src │ ├── type-tests │ │ ├── component-like.ts │ │ └── reactive-actor-from.ts │ ├── index.ts │ ├── registration.js │ ├── -private │ │ └── statechart-manager.js │ └── public-types.d.ts ├── .prettierrc.cjs ├── .eslintrc.cjs ├── addon-main.cjs ├── tsconfig.json ├── vite.config.mjs └── package.json ├── test-app ├── .watchmanconfig ├── types │ └── index.d.ts ├── public │ └── robots.txt ├── app │ ├── styles │ │ └── app.css │ ├── config │ │ ├── environment.js │ │ └── environment.d.ts │ ├── templates │ │ ├── application.gts │ │ └── demos │ │ │ └── toggle.gts │ ├── router.ts │ └── app.ts ├── .prettierignore ├── config │ ├── targets.js │ ├── optional-features.json │ ├── ember-cli-update.json │ ├── environment.js │ └── ember-try.js ├── .ember-cli ├── ember-cli-build.js ├── .template-lintrc.js ├── .editorconfig ├── .gitignore ├── tests │ ├── test-helper.ts │ ├── integration │ │ ├── modifier-test.gts │ │ ├── timing-test.gts │ │ ├── dynamic-machines-test.gts │ │ └── usage-test.gts │ └── index.html ├── testem.js ├── index.html ├── .prettierrc.cjs ├── tsconfig.json ├── babel.config.cjs ├── vite.config.mjs ├── package.json └── eslint.config.mjs ├── pnpm-workspace.yaml ├── logo-dark.png ├── .github ├── renovate.json5 └── workflows │ ├── codeql.yml │ ├── publish.yml │ ├── ci.yml │ └── plan-release.yml ├── .npmrc ├── .editorconfig ├── .gitignore ├── CONTRIBUTING.md ├── .prettierrc.cjs ├── LICENSE.md ├── .release-plan.json ├── RELEASE.md ├── package.json ├── README.md └── CHANGELOG.md /ember-statechart-component/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /test-app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["dist"] 3 | } 4 | -------------------------------------------------------------------------------- /ember-statechart-component/.gitignore: -------------------------------------------------------------------------------- 1 | declarations/ 2 | dist/ 3 | 4 | *.md 5 | -------------------------------------------------------------------------------- /test-app/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "ember-statechart-component" 3 | - "test-app" 4 | -------------------------------------------------------------------------------- /test-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NullVoxPopuli/ember-statechart-component/HEAD/logo-dark.png -------------------------------------------------------------------------------- /test-app/app/styles/app.css: -------------------------------------------------------------------------------- 1 | /* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */ 2 | -------------------------------------------------------------------------------- /test-app/app/config/environment.js: -------------------------------------------------------------------------------- 1 | import loadConfigFromMeta from '@embroider/config-meta-loader'; 2 | 3 | export default loadConfigFromMeta('test-app'); 4 | -------------------------------------------------------------------------------- /test-app/.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /dist/ 6 | 7 | # misc 8 | /coverage/ 9 | !.* 10 | .*/ 11 | 12 | # ember-try 13 | /.node_modules.ember-try/ 14 | -------------------------------------------------------------------------------- /test-app/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions', 7 | ]; 8 | 9 | module.exports = { 10 | browsers, 11 | }; 12 | -------------------------------------------------------------------------------- /test-app/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true, 6 | "no-implicit-route-model": true 7 | } 8 | -------------------------------------------------------------------------------- /test-app/.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript 4 | rather than JavaScript by default, when a TypeScript version of a given blueprint is available. 5 | */ 6 | "isTypeScriptProject": true 7 | } 8 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | // Docs: 2 | // https://docs.renovatebot.com/configuration-options/ 3 | { 4 | "extends": [ 5 | "github>NullVoxPopuli/renovate:npm.json5" 6 | ], 7 | "packageRules": [ 8 | { 9 | "matchPaths": ["testing/**"], 10 | "enabled": false 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | resolution-mode=highest 2 | 3 | # super strict mode 4 | auto-install-peers=false 5 | resolve-peers-from-workspace-root=false 6 | 7 | # default is true, we do this to try to have more isolation 8 | # since we test with incompatible sets of TS types. 9 | ; shared-workspace-lockfile=false 10 | 11 | -------------------------------------------------------------------------------- /test-app/ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | const { maybeEmbroider } = require('@embroider/test-setup'); 5 | 6 | module.exports = function (defaults) { 7 | let app = new EmberApp(defaults, {}); 8 | 9 | return maybeEmbroider(app); 10 | }; 11 | -------------------------------------------------------------------------------- /test-app/app/templates/application.gts: -------------------------------------------------------------------------------- 1 | import Route from 'ember-route-template'; 2 | import { pageTitle } from 'ember-page-title'; 3 | 4 | import { Toggler } from './demos/toggle'; 5 | 6 | export default Route( 7 | 14 | ); 15 | -------------------------------------------------------------------------------- /test-app/app/router.ts: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'test-app/config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function () { 10 | // Add route declarations here 11 | }); 12 | -------------------------------------------------------------------------------- /test-app/.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | overrides: [ 6 | { 7 | files: ['**/*'], 8 | rules: { 9 | 'require-input-label': 'off', 10 | 'no-block-params-for-html-elements': 'off', 11 | 'no-passed-in-event-handlers': 'off', 12 | }, 13 | }, 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /ember-statechart-component/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | [ 4 | "@babel/plugin-transform-typescript", 5 | { 6 | "allExtensions": true, 7 | "onlyRemoveTypeImports": true, 8 | "allowDeclareFields": true 9 | } 10 | ], 11 | ["module:decorator-transforms", { "runtime": { "import": "decorator-transforms/runtime" } }] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test-app/app/config/environment.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type declarations for 3 | * import config from 'test-app/config/environment' 4 | */ 5 | declare const config: { 6 | environment: string; 7 | modulePrefix: string; 8 | podModulePrefix: string; 9 | locationType: 'history' | 'hash' | 'none'; 10 | rootURL: string; 11 | APP: Record; 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /test-app/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "@embroider/app-blueprint", 6 | "version": "0.12.0", 7 | "blueprints": [ 8 | { 9 | "name": "@embroider/app-blueprint", 10 | "isBaseBlueprint": true, 11 | "options": ["--package-manager pnpm"] 12 | } 13 | ] 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /test-app/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /test-app/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /declarations/ 4 | /tmp/ 5 | 6 | # dependencies 7 | /node_modules/ 8 | 9 | # misc 10 | /.env* 11 | /.pnp* 12 | /.eslintcache 13 | /coverage/ 14 | /npm-debug.log* 15 | /testem.log 16 | /yarn-error.log 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /npm-shrinkwrap.json.ember-try 21 | /package.json.ember-try 22 | /package-lock.json.ember-try 23 | /yarn.lock.ember-try 24 | 25 | # broccoli-debug 26 | /DEBUG/ 27 | -------------------------------------------------------------------------------- /ember-statechart-component/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.hbs] 16 | insert_final_newline = false 17 | 18 | [*.{diff,md}] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist/ 5 | /tmp/ 6 | declarations/ 7 | .eslintcache 8 | 9 | # dependencies 10 | node_modules/ 11 | 12 | # misc 13 | /.env* 14 | /.pnp* 15 | /.sass-cache 16 | /.eslintcache 17 | /connect.lock 18 | /coverage/ 19 | /libpeerconnection.log 20 | /npm-debug.log* 21 | /testem.log 22 | /yarn-error.log 23 | 24 | # ember-try 25 | /.node_modules.ember-try/ 26 | /bower.json.ember-try 27 | /package.json.ember-try 28 | -------------------------------------------------------------------------------- /test-app/tests/test-helper.ts: -------------------------------------------------------------------------------- 1 | import 'qunit-theme-ember/qunit.css'; 2 | import 'ember-statechart-component'; 3 | 4 | import Application from 'test-app/app'; 5 | import config from 'test-app/config/environment'; 6 | import * as QUnit from 'qunit'; 7 | import { setApplication } from '@ember/test-helpers'; 8 | import { setup } from 'qunit-dom'; 9 | import { start as qunitStart } from 'ember-qunit'; 10 | 11 | export function start() { 12 | setApplication(Application.create(config.APP)); 13 | 14 | setup(QUnit.assert); 15 | 16 | qunitStart(); 17 | } 18 | -------------------------------------------------------------------------------- /ember-statechart-component/src/type-tests/component-like.ts: -------------------------------------------------------------------------------- 1 | import { expectTypeOf } from 'expect-type'; 2 | import { createMachine } from 'xstate'; 3 | 4 | import type { ComponentLike } from '@glint/template'; 5 | 6 | expectTypeOf( 7 | createMachine({ 8 | initial: 'inactive', 9 | states: { 10 | inactive: { on: { TOGGLE: 'active' } }, 11 | active: { on: { TOGGLE: 'inactive' } }, 12 | }, 13 | }) 14 | ).toMatchTypeOf< 15 | ComponentLike<{ 16 | Args: any; 17 | Blocks: { 18 | default: [any, any]; 19 | }; 20 | }> 21 | >(); 22 | -------------------------------------------------------------------------------- /test-app/app/app.ts: -------------------------------------------------------------------------------- 1 | import 'ember-statechart-component'; 2 | import Application from '@ember/application'; 3 | import compatModules from '@embroider/virtual/compat-modules'; 4 | import Resolver from 'ember-resolver'; 5 | import loadInitializers from 'ember-load-initializers'; 6 | import config from './config/environment'; 7 | 8 | export default class App extends Application { 9 | modulePrefix = config.modulePrefix; 10 | podModulePrefix = config.podModulePrefix; 11 | Resolver = Resolver.withModules(compatModules); 12 | } 13 | 14 | loadInitializers(App, config.modulePrefix, compatModules); 15 | -------------------------------------------------------------------------------- /ember-statechart-component/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | printWidth: 100, 5 | overrides: [ 6 | { 7 | // Lol, JavaScript 8 | files: ['*.js', '*.ts', '*.cjs', '.mjs', '.cts', '.mts', '.cts'], 9 | options: { 10 | singleQuote: true, 11 | trailingComma: 'es5', 12 | }, 13 | }, 14 | { 15 | files: ['*.json'], 16 | options: { 17 | singleQuote: false, 18 | }, 19 | }, 20 | { 21 | files: ['*.hbs'], 22 | options: { 23 | singleQuote: false, 24 | }, 25 | }, 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /ember-statechart-component/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { configs } = require('@nullvoxpopuli/eslint-configs'); 4 | 5 | const config = configs.ember(); 6 | 7 | module.exports = { 8 | ...config, 9 | overrides: [ 10 | ...config.overrides, 11 | { 12 | files: ['**/*.gts'], 13 | plugins: ['ember'], 14 | parser: 'ember-eslint-parser', 15 | }, 16 | { 17 | files: ['**/*.gjs'], 18 | plugins: ['ember'], 19 | parser: 'ember-eslint-parser', 20 | }, 21 | { 22 | files: ['**/*.ts'], 23 | rules: { 24 | '@typescript-eslint/ban-types': 'off', 25 | '@typescript-eslint/no-explicit-any': 'off', 26 | }, 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /ember-statechart-component/src/index.ts: -------------------------------------------------------------------------------- 1 | import './registration.js'; 2 | import './public-types.d.ts'; 3 | 4 | import { assert } from '@ember/debug'; 5 | import { getOwner } from '@ember/owner'; 6 | 7 | import type { Registry } from '@ember/service'; 8 | 9 | export { UPDATE_EVENT_NAME } from './-private/statechart-manager.js'; 10 | 11 | export function getService( 12 | context: unknown, 13 | serviceName: Key 14 | ): Registry[Key] { 15 | let owner = getOwner(context as object); 16 | 17 | assert(`Expected passed context to be aware of the container (owner)`, owner); 18 | 19 | let service = owner.lookup(`service:${serviceName}`) as Registry[Key]; 20 | 21 | return service; 22 | } 23 | -------------------------------------------------------------------------------- /ember-statechart-component/addon-main.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * ember-cli does not support ESM... 5 | * https://github.com/ember-cli/ember-cli/issues/9682 6 | * 7 | * Because of this, 8 | * - this file is renamed to *.cjs 9 | * - config in package.json must also be renamed to *.cjs 10 | * - about 3 places 11 | */ 12 | 13 | // import { dirname } from 'path'; 14 | // import { fileURLToPath } from 'url'; 15 | 16 | // import { addonV1Shim } from '@embroider/addon-shim'; 17 | 18 | // const __dirname = dirname(fileURLToPath(import.meta.url)); 19 | // export default addonV1Shim(__dirname); 20 | 21 | const { addonV1Shim } = require('@embroider/addon-shim'); 22 | 23 | module.exports = addonV1Shim(__dirname); 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd ember-statechart-component` 7 | * `npm install` 8 | 9 | ## Linting 10 | 11 | * `npm run lint` 12 | * `npm run lint:fix` 13 | 14 | ## Running tests 15 | 16 | * `ember test` – Runs the test suite on the current Ember version 17 | * `ember test --server` – Runs the test suite in "watch mode" 18 | * `ember try:each` – Runs the test suite against multiple Ember versions 19 | 20 | ## Running the test application 21 | 22 | * `ember serve` 23 | * Visit the test application at [http://localhost:4200](http://localhost:4200). 24 | 25 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 26 | -------------------------------------------------------------------------------- /test-app/testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof module !== 'undefined') { 4 | module.exports = { 5 | test_page: 'tests/index.html?hidepassed', 6 | disable_watching: true, 7 | launch_in_ci: ['Chrome'], 8 | launch_in_dev: ['Chrome'], 9 | browser_start_timeout: 120, 10 | browser_args: { 11 | Chrome: { 12 | ci: [ 13 | // --no-sandbox is needed when running Chrome inside a container 14 | process.env.CI ? '--no-sandbox' : null, 15 | '--headless=new', 16 | '--disable-dev-shm-usage', 17 | '--disable-software-rasterizer', 18 | '--mute-audio', 19 | '--remote-debugging-port=0', 20 | '--window-size=1440,900', 21 | ].filter(Boolean), 22 | }, 23 | }, 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /ember-statechart-component/src/type-tests/reactive-actor-from.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { expectTypeOf } from 'expect-type'; 3 | import { createMachine } from 'xstate'; 4 | 5 | import type { ReactiveActorFrom } from 'ember-statechart-component'; 6 | 7 | const Toggle = createMachine({ 8 | initial: 'inactive', 9 | states: { 10 | inactive: { on: { TOGGLE: 'active' } }, 11 | active: { on: { TOGGLE: 'inactive' } }, 12 | }, 13 | }); 14 | 15 | expectTypeOf>().not.toBeAny(); 16 | expectTypeOf>().toHaveProperty('statePath'); 17 | expectTypeOf>().toHaveProperty('send'); 18 | 19 | expectTypeOf['send']>().toMatchTypeOf<(type: string) => void>(); 20 | -------------------------------------------------------------------------------- /test-app/app/templates/demos/toggle.gts: -------------------------------------------------------------------------------- 1 | import { createMachine } from 'xstate'; 2 | import { on } from '@ember/modifier'; 3 | import { fn } from '@ember/helper'; 4 | 5 | import type { ReactiveActorFrom } from 'ember-statechart-component'; 6 | 7 | const Toggle = createMachine({ 8 | initial: 'inactive', 9 | states: { 10 | inactive: { on: { TOGGLE: 'active' } }, 11 | active: { on: { TOGGLE: 'inactive' } }, 12 | }, 13 | }); 14 | 15 | function allCapsState(machine: ReactiveActorFrom) { 16 | return machine.statePath; 17 | } 18 | 19 | export const Toggler = ; 30 | -------------------------------------------------------------------------------- /test-app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AppTemplate 6 | 7 | 8 | 9 | {{content-for "head"}} 10 | 11 | 12 | 13 | 14 | {{content-for "head-footer"}} 15 | 16 | 17 | {{content-for "body"}} 18 | 19 | 20 | 26 | 27 | {{content-for "body-footer"}} 28 | 29 | 30 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | plugins: ['prettier-plugin-ember-template-tag'], 5 | singleQuote: true, 6 | overrides: [ 7 | { 8 | files: ['*.js', '*.ts', '*.cjs', '.mjs', '.cts', '.mts', '.cts'], 9 | options: { 10 | singleQuote: true, 11 | trailingComma: 'es5', 12 | }, 13 | }, 14 | { 15 | files: ['*.html'], 16 | options: { 17 | singleQuote: false, 18 | }, 19 | }, 20 | { 21 | files: ['*.json'], 22 | options: { 23 | singleQuote: false, 24 | }, 25 | }, 26 | { 27 | files: ['*.hbs'], 28 | options: { 29 | singleQuote: false, 30 | }, 31 | }, 32 | { 33 | files: ['*.gjs', '*.gts'], 34 | options: { 35 | singleQuote: true, 36 | templateSingleQuote: false, 37 | trailingComma: 'es5', 38 | }, 39 | }, 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /test-app/.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | plugins: ['prettier-plugin-ember-template-tag'], 5 | singleQuote: true, 6 | overrides: [ 7 | { 8 | files: ['*.js', '*.ts', '*.cjs', '.mjs', '.cts', '.mts', '.cts'], 9 | options: { 10 | singleQuote: true, 11 | trailingComma: 'es5', 12 | }, 13 | }, 14 | { 15 | files: ['*.html'], 16 | options: { 17 | singleQuote: false, 18 | }, 19 | }, 20 | { 21 | files: ['*.json'], 22 | options: { 23 | singleQuote: false, 24 | }, 25 | }, 26 | { 27 | files: ['*.hbs'], 28 | options: { 29 | singleQuote: false, 30 | }, 31 | }, 32 | { 33 | files: ['*.gjs', '*.gts'], 34 | options: { 35 | singleQuote: true, 36 | templateSingleQuote: false, 37 | trailingComma: 'es5', 38 | }, 39 | }, 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: "38 6 * * 5" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v6 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v4 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v4 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v4 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /test-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/ember/tsconfig.json", 3 | "include": ["app", "tests", "types"], 4 | "glint": { 5 | "environment": ["ember-loose", "ember-template-imports"] 6 | }, 7 | "compilerOptions": { 8 | "allowJs": true, 9 | /** 10 | https://www.typescriptlang.org/tsconfig#noEmitOnError 11 | Do not block emit on TS errors. 12 | */ 13 | "noEmitOnError": false, 14 | 15 | "declaration": false, 16 | "declarationMap": false, 17 | 18 | /** 19 | https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions 20 | 21 | We want our tooling to know how to resolve our custom files so the appropriate plugins 22 | can do the proper transformations on those files. 23 | */ 24 | "allowImportingTsExtensions": true, 25 | 26 | // Stylelistic 27 | "noPropertyAccessFromIndexSignature": false, 28 | "paths": { 29 | "test-app/tests/*": ["./tests/*"], 30 | "test-app/*": ["./app/*"], 31 | "*": ["./types/*"] 32 | }, 33 | "types": ["ember-source/types"] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /.release-plan.json: -------------------------------------------------------------------------------- 1 | { 2 | "solution": { 3 | "ember-statechart-component": { 4 | "impact": "minor", 5 | "oldVersion": "7.0.0", 6 | "newVersion": "7.1.0", 7 | "constraints": [ 8 | { 9 | "impact": "minor", 10 | "reason": "Appears in changelog section :rocket: Enhancement" 11 | }, 12 | { 13 | "impact": "patch", 14 | "reason": "Appears in changelog section :bug: Bug Fix" 15 | } 16 | ], 17 | "pkgJSONPath": "./ember-statechart-component/package.json" 18 | } 19 | }, 20 | "description": "## Release (2024-10-19)\n\nember-statechart-component 7.1.0 (minor)\n\n#### :rocket: Enhancement\n* `ember-statechart-component`\n * [#502](https://github.com/NullVoxPopuli/ember-statechart-component/pull/502) Expose wrapper type ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### :bug: Bug Fix\n* `ember-statechart-component`\n * [#503](https://github.com/NullVoxPopuli/ember-statechart-component/pull/503) Fix .matches alias method ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### Committers: 1\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n" 21 | } 22 | -------------------------------------------------------------------------------- /ember-statechart-component/src/registration.js: -------------------------------------------------------------------------------- 1 | import { setComponentManager, setComponentTemplate } from '@ember/component'; 2 | import { precompileTemplate } from '@ember/template-compilation'; 3 | 4 | import { StateMachine } from 'xstate'; 5 | 6 | import ComponentManager from './-private/statechart-manager.js'; 7 | 8 | let isSetup = false; 9 | 10 | /** 11 | * Installs StateNode detection for Ember. 12 | * This is what allows Ember to render createMachine() values as components. 13 | * 14 | * Optionally, if, for whatever reason, there is a risk of multiple XState's 15 | * in the dependency graph (like with linking dependencies), an `override` may 16 | * be passed to choose a specific XState (hopefully from the host app). 17 | */ 18 | export function setupComponentMachines(override) { 19 | if (isSetup) return; 20 | 21 | // Managers are managed globally, and not per app instance 22 | setComponentManager( 23 | (owner) => ComponentManager.create(owner), 24 | override?.prototype || StateMachine.prototype 25 | ); 26 | 27 | setComponentTemplate( 28 | precompileTemplate(`{{yield this}}`, { 29 | strictMode: true, 30 | }), 31 | override?.prototype || StateMachine.prototype 32 | ); 33 | 34 | isSetup = true; 35 | } 36 | 37 | // ‼️ SideEffect 38 | setupComponentMachines(); 39 | -------------------------------------------------------------------------------- /test-app/babel.config.cjs: -------------------------------------------------------------------------------- 1 | const { 2 | babelCompatSupport, 3 | templateCompatSupport, 4 | } = require('@embroider/compat/babel'); 5 | 6 | module.exports = { 7 | plugins: [ 8 | [ 9 | '@babel/plugin-transform-typescript', 10 | { 11 | allExtensions: true, 12 | onlyRemoveTypeImports: true, 13 | allowDeclareFields: true, 14 | }, 15 | ], 16 | [ 17 | 'babel-plugin-ember-template-compilation', 18 | { 19 | compilerPath: 'ember-source/dist/ember-template-compiler.js', 20 | enableLegacyModules: [ 21 | 'ember-cli-htmlbars', 22 | 'ember-cli-htmlbars-inline-precompile', 23 | 'htmlbars-inline-precompile', 24 | ], 25 | transforms: [...templateCompatSupport()], 26 | }, 27 | ], 28 | [ 29 | 'module:decorator-transforms', 30 | { 31 | runtime: { 32 | import: require.resolve('decorator-transforms/runtime-esm'), 33 | }, 34 | }, 35 | ], 36 | [ 37 | '@babel/plugin-transform-runtime', 38 | { 39 | absoluteRuntime: __dirname, 40 | useESModules: true, 41 | regenerator: false, 42 | }, 43 | ], 44 | ...babelCompatSupport(), 45 | ], 46 | 47 | generatorOpts: { 48 | compact: false, 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /test-app/tests/integration/modifier-test.gts: -------------------------------------------------------------------------------- 1 | import { render } from '@ember/test-helpers'; 2 | import { module, test } from 'qunit'; 3 | import { setupRenderingTest } from 'ember-qunit'; 4 | import { fn } from '@ember/helper'; 5 | import * as em from 'ember-modifier'; 6 | 7 | import { createMachine } from 'xstate'; 8 | 9 | module('Modifiers', function (hooks) { 10 | setupRenderingTest(hooks); 11 | 12 | test('a modifier can trigger an update to a machine', async function (assert) { 13 | const customModifier = em.modifier( 14 | (_element: Element, [toggle]: [() => void]) => { 15 | toggle(); 16 | } 17 | ); 18 | 19 | const ToggleMachine = createMachine({ 20 | initial: 'inactive', 21 | states: { 22 | inactive: { on: { TOGGLE: 'active' } }, 23 | active: { on: { TOGGLE: 'inactive' } }, 24 | }, 25 | }); 26 | 27 | await render( 28 | 37 | ); 38 | 39 | assert.dom().doesNotContainText('inactive'); 40 | assert.dom().containsText('active'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test-app/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { 3 | resolver, 4 | hbs, 5 | scripts, 6 | templateTag, 7 | optimizeDeps, 8 | compatPrebuild, 9 | assets, 10 | contentFor, 11 | } from '@embroider/vite'; 12 | import { babel } from '@rollup/plugin-babel'; 13 | 14 | const extensions = [ 15 | '.mjs', 16 | '.gjs', 17 | '.js', 18 | '.mts', 19 | '.gts', 20 | '.ts', 21 | '.hbs', 22 | '.json', 23 | ]; 24 | 25 | export default defineConfig(({ mode }) => { 26 | return { 27 | resolve: { 28 | extensions, 29 | }, 30 | plugins: [ 31 | hbs(), 32 | templateTag(), 33 | scripts(), 34 | resolver(), 35 | compatPrebuild(), 36 | assets(), 37 | contentFor(), 38 | 39 | babel({ 40 | babelHelpers: 'runtime', 41 | extensions, 42 | }), 43 | ], 44 | optimizeDeps: optimizeDeps(), 45 | server: { 46 | port: 4200, 47 | }, 48 | build: { 49 | outDir: 'dist', 50 | rollupOptions: { 51 | input: { 52 | main: 'index.html', 53 | ...(shouldBuildTests(mode) 54 | ? { tests: 'tests/index.html' } 55 | : undefined), 56 | }, 57 | }, 58 | }, 59 | }; 60 | }); 61 | 62 | function shouldBuildTests(mode) { 63 | return mode !== 'production' || process.env.FORCE_BUILD_TESTS; 64 | } 65 | -------------------------------------------------------------------------------- /test-app/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | const ENV = { 5 | modulePrefix: 'test-app', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'history', 9 | EmberENV: { 10 | EXTEND_PROTOTYPES: false, 11 | FEATURES: { 12 | // Here you can enable experimental features on an ember canary build 13 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 14 | }, 15 | }, 16 | 17 | APP: { 18 | // Here you can pass flags/options to your application instance 19 | // when it is created 20 | }, 21 | }; 22 | 23 | if (environment === 'development') { 24 | // ENV.APP.LOG_RESOLVER = true; 25 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 26 | // ENV.APP.LOG_TRANSITIONS = true; 27 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 28 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 29 | } 30 | 31 | if (environment === 'test') { 32 | // Testem prefers this... 33 | ENV.locationType = 'none'; 34 | 35 | // keep test console output quieter 36 | ENV.APP.LOG_ACTIVE_GENERATION = false; 37 | ENV.APP.LOG_VIEW_LOOKUPS = false; 38 | 39 | ENV.APP.rootElement = '#ember-testing'; 40 | ENV.APP.autoboot = false; 41 | } 42 | 43 | if (environment === 'production') { 44 | // here you can enable a production-specific feature 45 | } 46 | 47 | return ENV; 48 | }; 49 | -------------------------------------------------------------------------------- /test-app/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AppTemplate Tests 6 | 7 | 8 | 9 | {{content-for "head"}} {{content-for "test-head"}} 10 | 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} {{content-for "test-head-footer"}} 16 | 17 | 18 | {{content-for "body"}} {{content-for "test-body"}} 19 | 20 |
21 |
22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 33 | 34 | 39 | 40 | {{content-for "body-footer"}} 41 | 42 | 43 | -------------------------------------------------------------------------------- /test-app/tests/integration/timing-test.gts: -------------------------------------------------------------------------------- 1 | import { render } from '@ember/test-helpers'; 2 | import { module, test } from 'qunit'; 3 | import { setupRenderingTest } from 'ember-qunit'; 4 | 5 | import { fromCallback, createMachine, sendTo } from 'xstate'; 6 | 7 | module('Timing', function (hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | module('Infinite (Re|In)validation messaging', function () { 11 | test('Can have an initial state with invoke', async function (assert) { 12 | const Machine = createMachine({ 13 | initial: 'waiting', 14 | states: { 15 | stateA: {}, 16 | waiting: { 17 | invoke: { 18 | id: 'transition-away-immediately', 19 | src: fromCallback(({ sendBack }) => { 20 | sendBack({ type: 'IMPLICTLY_NEXT' }); 21 | }), 22 | input: ({ self }) => ({ parent: self }), 23 | onDone: {}, 24 | onError: {}, 25 | }, 26 | entry: sendTo('transition-away-immediately', ({ self }) => ({ 27 | sender: self, 28 | })), 29 | on: { 30 | IMPLICTLY_NEXT: 'stateA', 31 | }, 32 | }, 33 | }, 34 | }); 35 | 36 | await render( 37 | 42 | ); 43 | 44 | assert.dom().containsText('stateA'); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /test-app/config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = async function () { 6 | return { 7 | usePnpm: true, 8 | scenarios: [ 9 | { 10 | name: 'ember-5.1', 11 | npm: { 12 | devDependencies: { 13 | 'ember-source': '~5.1.0', 14 | }, 15 | }, 16 | }, 17 | { 18 | name: 'ember-5.4', 19 | npm: { 20 | devDependencies: { 21 | 'ember-source': '~5.4.0', 22 | }, 23 | }, 24 | }, 25 | { 26 | name: 'ember-5.8', 27 | npm: { 28 | devDependencies: { 29 | 'ember-source': '~5.8.0', 30 | }, 31 | }, 32 | }, 33 | { 34 | name: 'ember-5.12', 35 | npm: { 36 | devDependencies: { 37 | 'ember-source': '~5.12.0', 38 | }, 39 | }, 40 | }, 41 | { 42 | name: 'ember-release', 43 | npm: { 44 | devDependencies: { 45 | 'ember-source': await getChannelURL('release'), 46 | }, 47 | }, 48 | }, 49 | { 50 | name: 'ember-beta', 51 | npm: { 52 | devDependencies: { 53 | 'ember-source': await getChannelURL('beta'), 54 | }, 55 | }, 56 | }, 57 | { 58 | name: 'ember-canary', 59 | npm: { 60 | devDependencies: { 61 | 'ember-source': await getChannelURL('canary'), 62 | }, 63 | }, 64 | }, 65 | ], 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /ember-statechart-component/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/ember/tsconfig.json", 3 | "glint": { 4 | "environment": ["ember-loose", "ember-template-imports"] 5 | }, 6 | "include": ["src"], 7 | "compilerOptions": { 8 | "allowJs": true, 9 | // Don't typecheck addon-shim 10 | // "skipLibCheck": true, 11 | "declarationDir": "declarations", 12 | 13 | /** 14 | https://www.typescriptlang.org/tsconfig#rootDir 15 | "Default: The longest common path of all non-declaration input files." 16 | 17 | Because we want our declarations' structure to match our rollup output, 18 | we need this "rootDir" to match the "srcDir" in the rollup.config.mjs. 19 | 20 | This way, we can have simpler `package.json#exports` that matches 21 | imports to files on disk 22 | */ 23 | "rootDir": "./src", 24 | 25 | /** 26 | https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax 27 | 28 | We don't want to include types dependencies in our compiled output, so tell TypeScript 29 | to enforce using `import type` instead of `import` for Types. 30 | */ 31 | "verbatimModuleSyntax": true, 32 | 33 | /** 34 | https://www.typescriptlang.org/tsconfig#allowImportingTsExtensions 35 | 36 | We want our tooling to know how to resolve our custom files so the appropriate plugins 37 | can do the proper transformations on those files. 38 | */ 39 | "allowImportingTsExtensions": true, 40 | 41 | // Stylelistic 42 | "noPropertyAccessFromIndexSignature": false, 43 | 44 | "types": ["../test-app/node_modules/ember-source/types/stable"] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | Releases in this repo are mostly automated using [release-plan](https://github.com/embroider-build/release-plan/). Once you label all your PRs correctly (see below) you will have an automatically generated PR that updates your CHANGELOG.md file and a `.release-plan.json` that is used to prepare the release once the PR is merged. 4 | 5 | ## Preparation 6 | 7 | Since the majority of the actual release process is automated, the remaining tasks before releasing are: 8 | 9 | - correctly labeling **all** pull requests that have been merged since the last release 10 | - updating pull request titles so they make sense to our users 11 | 12 | Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.1.0/), but the overall 13 | guiding principle here is that changelogs are for humans, not machines. 14 | 15 | When reviewing merged PR's the labels to be used are: 16 | 17 | * breaking - Used when the PR is considered a breaking change. 18 | * enhancement - Used when the PR adds a new feature or enhancement. 19 | * bug - Used when the PR fixes a bug included in a previous release. 20 | * documentation - Used when the PR adds or updates documentation. 21 | * internal - Internal changes or things that don't fit in any other category. 22 | 23 | **Note:** `release-plan` requires that **all** PRs are labeled. If a PR doesn't fit in a category it's fine to label it as `internal` 24 | 25 | ## Release 26 | 27 | Once the prep work is completed, the actual release is straight forward: you just need to merge the open [Plan Release](https://github.com/NullVoxPopuli/ember-statechart-component/pulls?q=is%3Apr+is%3Aopen+%22Prepare+Release%22+in%3Atitle) PR 28 | -------------------------------------------------------------------------------- /ember-statechart-component/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import { readFile } from "node:fs/promises"; 2 | import { join, resolve } from "node:path"; 3 | 4 | import { babel } from "@rollup/plugin-babel"; 5 | import { defineConfig } from "vite"; 6 | 7 | const manifestStr = await readFile(join(import.meta.dirname, "package.json")); 8 | const manifest = JSON.parse(manifestStr); 9 | // Why is this not default? 10 | // Why else would you specify (peer)deps? 11 | const externals = [ 12 | ...Object.keys(manifest.dependencies ?? {}), 13 | ...Object.keys(manifest.peerDependencies ?? {}), 14 | ]; 15 | 16 | export default defineConfig({ 17 | build: { 18 | outDir: "dist", 19 | // These targets are not "support". 20 | // A consuming app or library should compile further if they need to support 21 | // old browsers. 22 | target: ["esnext", "firefox121"], 23 | minify: false, 24 | sourcemap: true, 25 | lib: { 26 | // Could also be a dictionary or array of multiple entry points 27 | entry: resolve(import.meta.dirname, "src/index.ts"), 28 | name: "ember-statechart-component", 29 | formats: ["es"], 30 | // the proper extensions will be added 31 | fileName: "index", 32 | }, 33 | rollupOptions: { 34 | external: [ 35 | ...externals, 36 | "@ember/application", 37 | "@ember/component", 38 | "@ember/debug", 39 | "@ember/helper", 40 | "@ember/modifier", 41 | "@ember/owner", 42 | "@ember/runloop", 43 | "@ember/destroyable", 44 | "@ember/template-compilation", 45 | "@glimmer/tracking", 46 | "@glint/template", 47 | ], 48 | }, 49 | }, 50 | plugins: [ 51 | babel({ 52 | babelHelpers: "bundled", 53 | extensions: [".js", ".ts"], 54 | }), 55 | ], 56 | }); 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "repository": "https://github.com/NullVoxPopuli/ember-statechart-component", 4 | "license": "MIT", 5 | "author": "NullVoxPopuli", 6 | "workspaces": [ 7 | "ember-statechart-component", 8 | "testing/*" 9 | ], 10 | "scripts": { 11 | "build": "pnpm --filter ember-statechart-component build", 12 | "prepare": "pnpm build", 13 | "lint": "pnpm run --filter '*' lint", 14 | "lint:fix": "pnpm run --filter '*' lint:fix" 15 | }, 16 | "devDependencies": { 17 | "@babel/core": "7.25.7", 18 | "@glint/core": "1.4.1-unstable.b29a807", 19 | "@glint/environment-ember-loose": "1.4.1-unstable.b29a807", 20 | "@glint/environment-ember-template-imports": "1.4.1-unstable.b29a807", 21 | "@glint/template": "1.4.1-unstable.b29a807", 22 | "@typescript-eslint/parser": "^8.8.0", 23 | "ember-template-lint": "^6.0.0", 24 | "eslint": "^8.56.0", 25 | "eslint-plugin-ember": "^12.2.1", 26 | "eslint-plugin-node": "^11.1.0", 27 | "prettier": "^3.3.3", 28 | "release-plan": "^0.9.0", 29 | "typescript": "^5.5.0" 30 | }, 31 | "packageManager": "pnpm@9.15.9", 32 | "volta": { 33 | "node": "24.11.1", 34 | "pnpm": "9.15.9" 35 | }, 36 | "pnpm": { 37 | "overrides": { 38 | "@glimmer/component": "2.0.0", 39 | "@glint/core": "1.5.2", 40 | "@glint/environment-ember-loose": "1.5.2", 41 | "@glint/environment-ember-template-imports": "1.5.2", 42 | "@glint/template": "1.7.3", 43 | "ember-cli-babel": "^8.2.0", 44 | "ember-cli-typescript": "5.3.0" 45 | }, 46 | "peerDependencyRules": { 47 | "allowAny": [ 48 | "typescript", 49 | "ember-source" 50 | ], 51 | "ignoreMissing": [ 52 | "webpack", 53 | "rsvp" 54 | ] 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # For every push to the master branch, this checks if the release-plan was 2 | # updated and if it was it will publish stable npm packages based on the 3 | # release plan 4 | 5 | name: Publish Stable 6 | 7 | on: 8 | workflow_dispatch: 9 | push: 10 | branches: 11 | - main 12 | - master 13 | 14 | concurrency: 15 | group: publish-${{ github.head_ref || github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | check-plan: 20 | name: "Check Release Plan" 21 | runs-on: ubuntu-latest 22 | outputs: 23 | command: ${{ steps.check-release.outputs.command }} 24 | 25 | steps: 26 | - uses: actions/checkout@v6 27 | with: 28 | fetch-depth: 0 29 | ref: 'main' 30 | # This will only cause the `check-plan` job to have a result of `success` 31 | # when the .release-plan.json file was changed on the last commit. This 32 | # plus the fact that this action only runs on main will be enough of a guard 33 | - id: check-release 34 | run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT 35 | 36 | publish: 37 | name: "NPM Publish" 38 | runs-on: ubuntu-latest 39 | needs: check-plan 40 | if: needs.check-plan.outputs.command == 'release' 41 | permissions: 42 | contents: write 43 | pull-requests: write 44 | 45 | steps: 46 | - uses: actions/checkout@v6 47 | - uses: wyvox/action-setup-pnpm@v3 48 | with: 49 | # This creates an .npmrc that reads the NODE_AUTH_TOKEN environment variable 50 | node-registry-url: 'https://registry.npmjs.org' 51 | - name: npm publish 52 | run: pnpm release-plan publish 53 | env: 54 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} 55 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | "on": 3 | workflow_dispatch: {} 4 | pull_request: null 5 | push: 6 | branches: 7 | - main 8 | schedule: 9 | - cron: "0 3 * * 0 " 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | timeout-minutes: 5 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: true 18 | matrix: 19 | path: 20 | - ./ember-statechart-component 21 | - ./test-app 22 | steps: 23 | - uses: actions/checkout@v6 24 | - uses: wyvox/action-setup-pnpm@v3 25 | - run: cd ${{ matrix.path }} && pnpm run lint 26 | 27 | tests: 28 | name: Default Tests 29 | timeout-minutes: 5 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v6 33 | - uses: wyvox/action-setup-pnpm@v3 34 | - run: pnpm --filter test-app run test:ember 35 | 36 | floating-deps-tests: 37 | name: Floating Deps Test 38 | timeout-minutes: 5 39 | runs-on: ubuntu-latest 40 | needs: tests 41 | steps: 42 | - uses: actions/checkout@v6 43 | - uses: wyvox/action-setup-pnpm@v3 44 | with: 45 | args: '--no-lockfile' 46 | - run: pnpm --filter test-app run test:ember 47 | 48 | # This seems to not work at the moment -- can't connect to browser 49 | # try-scenarios: 50 | # name: ${{ matrix.ember-try-scenario }} 51 | # timeout-minutes: 10 52 | # runs-on: ubuntu-latest 53 | # needs: tests 54 | # strategy: 55 | # fail-fast: true 56 | # matrix: 57 | # ember-try-scenario: 58 | # - ember-5.1 59 | # - ember-5.4 60 | # - ember-5.8 61 | # - ember-5.12 62 | # - ember-release 63 | # - ember-beta 64 | # - ember-canary 65 | # steps: 66 | # - uses: actions/checkout@v4 67 | # - uses: wyvox/action-setup-pnpm@v3 68 | # - name: test 69 | # working-directory: ./test-app 70 | # run: node_modules/.bin/ember try:one ${{ matrix.ember-try-scenario }} 71 | # --skip-cleanup 72 | 73 | typescript-compatibility: 74 | name: ${{ matrix.typescript-scenario }} 75 | timeout-minutes: 5 76 | runs-on: ubuntu-latest 77 | continue-on-error: true 78 | needs: tests 79 | strategy: 80 | fail-fast: true 81 | matrix: 82 | typescript-scenario: 83 | - typescript@5.2 84 | - typescript@5.3 85 | - typescript@5.4 86 | - typescript@5.5 87 | - typescript@5.6 88 | steps: 89 | - uses: actions/checkout@v6 90 | - uses: wyvox/action-setup-pnpm@v3 91 | - name: Update TS Version 92 | run: pnpm add --save-dev ${{ matrix.typescript-scenario }} 93 | working-directory: ./test-app 94 | - name: Type checking 95 | run: >- 96 | pnpm --filter test-app exec tsc -v; 97 | pnpm --filter test-app exec glint --version; 98 | pnpm --filter test-app exec glint 99 | -------------------------------------------------------------------------------- /.github/workflows/plan-release.yml: -------------------------------------------------------------------------------- 1 | name: Release Plan Review 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - master 7 | pull_request_target: # This workflow has permissions on the repo, do NOT run code from PRs in this workflow. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 8 | types: 9 | - labeled 10 | - unlabeled 11 | 12 | concurrency: 13 | group: plan-release # only the latest one of these should ever be running 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | check-plan: 18 | name: "Check Release Plan" 19 | runs-on: ubuntu-latest 20 | outputs: 21 | command: ${{ steps.check-release.outputs.command }} 22 | 23 | steps: 24 | - uses: actions/checkout@v6 25 | with: 26 | fetch-depth: 0 27 | ref: 'main' 28 | # This will only cause the `check-plan` job to have a "command" of `release` 29 | # when the .release-plan.json file was changed on the last commit. 30 | - id: check-release 31 | run: if git diff --name-only HEAD HEAD~1 | grep -w -q ".release-plan.json"; then echo "command=release"; fi >> $GITHUB_OUTPUT 32 | 33 | prepare_release_notes: 34 | name: Prepare Release Notes 35 | runs-on: ubuntu-latest 36 | timeout-minutes: 5 37 | needs: check-plan 38 | permissions: 39 | contents: write 40 | issues: read 41 | pull-requests: write 42 | outputs: 43 | explanation: ${{ steps.explanation.outputs.text }} 44 | # only run on push event if plan wasn't updated (don't create a release plan when we're releasing) 45 | # only run on labeled event if the PR has already been merged 46 | if: (github.event_name == 'push' && needs.check-plan.outputs.command != 'release') || (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true) 47 | 48 | steps: 49 | - uses: actions/checkout@v6 50 | # We need to download lots of history so that 51 | # github-changelog can discover what's changed since the last release 52 | with: 53 | fetch-depth: 0 54 | ref: 'main' 55 | - uses: wyvox/action-setup-pnpm@v3 56 | - name: "Generate Explanation and Prep Changelogs" 57 | id: explanation 58 | run: | 59 | set +e 60 | pnpm release-plan prepare 2> >(tee -a release-plan-stderr.txt >&2) 61 | 62 | if [ $? -ne 0 ]; then 63 | echo 'text<> $GITHUB_OUTPUT 64 | cat release-plan-stderr.txt >> $GITHUB_OUTPUT 65 | echo 'EOF' >> $GITHUB_OUTPUT 66 | else 67 | echo 'text<> $GITHUB_OUTPUT 68 | jq .description .release-plan.json -r >> $GITHUB_OUTPUT 69 | echo 'EOF' >> $GITHUB_OUTPUT 70 | rm release-plan-stderr.txt 71 | fi 72 | env: 73 | GITHUB_AUTH: ${{ secrets.GITHUB_TOKEN }} 74 | 75 | - uses: peter-evans/create-pull-request@v7 76 | with: 77 | commit-message: "Prepare Release using 'release-plan'" 78 | labels: "internal" 79 | branch: release-preview 80 | title: Prepare Release 81 | body: | 82 | This PR is a preview of the release that [release-plan](https://github.com/embroider-build/release-plan) has prepared. To release you should just merge this PR 👍 83 | 84 | ----------------------------------------- 85 | 86 | ${{ steps.explanation.outputs.text }} 87 | -------------------------------------------------------------------------------- /ember-statechart-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-statechart-component", 3 | "version": "7.1.0", 4 | "keywords": [ 5 | "ember-addon" 6 | ], 7 | "type": "module", 8 | "description": "Use XState Machines *as* Components", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/NullVoxPopuli/ember-statechart-component.git" 12 | }, 13 | "license": "MIT", 14 | "author": "NullVoxPopuli", 15 | "exports": { 16 | ".": { 17 | "types": "./src/public-types.d.ts", 18 | "default": "./dist/index.js" 19 | }, 20 | "./addon-main.js": "./addon-main.cjs" 21 | }, 22 | "files": [ 23 | "addon-main.cjs", 24 | "dist", 25 | "src", 26 | "declarations", 27 | "CHANGELOG.md", 28 | "README.md" 29 | ], 30 | "scripts": { 31 | "start": "pnpm vite build --watch", 32 | "build": "pnpm vite build", 33 | "watch:js": "rollup -c --watch --no-watch.clearScreen", 34 | "lint": "concurrently \"npm:lint:*(!fix)\" --names \"lint:\"", 35 | "lint:package": "publint", 36 | "lint:published-types": "attw --pack --ignore-rules cjs-resolves-to-esm --exclude-entrypoints addon-main.js --ignore-rules no-resolution", 37 | "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\" && pnpm format:prettier", 38 | "lint:js": "eslint . --cache", 39 | "lint:js:fix": "eslint . --fix", 40 | "format:prettier": "prettier . --write", 41 | "lint:prettier": "prettier . --check", 42 | "prepare": "pnpm build", 43 | "prepack": "pnpm build" 44 | }, 45 | "dependencies": { 46 | "@ember/test-waiters": "^3.1.0 || ^4.0.0", 47 | "@embroider/addon-shim": "^1.8.9", 48 | "decorator-transforms": "^2.2.2" 49 | }, 50 | "peerDependencies": { 51 | "@glimmer/component": "^1.1.2", 52 | "@glint/template": ">= 1.0.0", 53 | "xstate": "^5.18.0" 54 | }, 55 | "devDependencies": { 56 | "@arethetypeswrong/cli": "^0.16.4", 57 | "@babel/core": "7.25.7", 58 | "@babel/eslint-parser": "^7.25.7", 59 | "@babel/plugin-proposal-class-properties": "7.18.6", 60 | "@babel/plugin-syntax-decorators": "7.25.7", 61 | "@babel/plugin-transform-typescript": "^7.25.7", 62 | "@babel/preset-typescript": "7.25.7", 63 | "@embroider/addon-dev": "^5.0.0", 64 | "@glimmer/compiler": "^0.92.4", 65 | "@glimmer/component": "^1.1.2", 66 | "@glimmer/env": "^0.1.7", 67 | "@glint/core": "1.4.1-unstable.b29a807", 68 | "@glint/environment-ember-loose": "1.4.1-unstable.b29a807", 69 | "@glint/environment-ember-template-imports": "1.4.1-unstable.b29a807", 70 | "@glint/template": "1.4.1-unstable.b29a807", 71 | "@nullvoxpopuli/eslint-configs": "^4.2.0", 72 | "@rollup/plugin-babel": "^6.0.4", 73 | "@tsconfig/ember": "^3.0.3", 74 | "@typescript-eslint/eslint-plugin": "^8.8.0", 75 | "@typescript-eslint/parser": "^8.8.0", 76 | "concurrently": "^9.0.1", 77 | "ember-source": "^5.5.0", 78 | "eslint": "^8.57.0", 79 | "eslint-plugin-decorator-position": "^5.0.2", 80 | "eslint-plugin-ember": "^12.2.1", 81 | "eslint-plugin-import": "^2.31.0", 82 | "eslint-plugin-json": "^4.0.1", 83 | "eslint-plugin-node": "^11.1.0", 84 | "eslint-plugin-simple-import-sort": "^12.1.1", 85 | "expect-type": "^1.1.0", 86 | "prettier": "^3.3.3", 87 | "publint": "^0.2.11", 88 | "typescript": "~5.5.0", 89 | "vite": "^5.4.8", 90 | "vite-plugin-dts": "^4.2.4", 91 | "xstate": "^5.18.2" 92 | }, 93 | "publishConfig": { 94 | "registry": "https://registry.npmjs.org" 95 | }, 96 | "ember": { 97 | "edition": "octane" 98 | }, 99 | "ember-addon": { 100 | "version": 2, 101 | "type": "addon", 102 | "main": "addon-main.cjs", 103 | "app-js": {} 104 | }, 105 | "volta": { 106 | "extends": "../package.json" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Small description for test-app goes here", 6 | "repository": "", 7 | "license": "MIT", 8 | "author": "", 9 | "directories": { 10 | "doc": "doc", 11 | "test": "tests" 12 | }, 13 | "scripts": { 14 | "build": "vite build", 15 | "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\" --prefixColors auto", 16 | "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\" --prefixColors auto && pnpm format:prettier", 17 | "lint:hbs": "ember-template-lint .", 18 | "lint:hbs:fix": "ember-template-lint . --fix", 19 | "lint:js": "eslint . --cache", 20 | "lint:js:fix": "eslint . --fix", 21 | "lint:types": "glint", 22 | "format:prettier": "prettier . --write", 23 | "lint:prettier": "prettier . --check", 24 | "start": "vite", 25 | "test:ember": "vite build --mode test && ember test --path dist" 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.25.2", 29 | "@babel/eslint-parser": "^7.25.7", 30 | "@babel/plugin-transform-typescript": "^7.25.7", 31 | "@ember/optional-features": "^2.1.0", 32 | "@ember/string": "^4.0.0", 33 | "@ember/test-helpers": "^3.3.1", 34 | "@embroider/compat": "3.6.2-unstable.89d7d7f", 35 | "@embroider/config-meta-loader": "0.0.1-unstable.89d7d7f", 36 | "@embroider/core": "3.4.16-unstable.89d7d7f", 37 | "@embroider/test-setup": "4.0.1-unstable.89d7d7f", 38 | "@embroider/vite": "0.2.1-unstable.89d7d7f", 39 | "@glimmer/component": "^1.1.2", 40 | "@glimmer/tracking": "^1.1.2", 41 | "@glint/core": "1.4.1-unstable.b29a807", 42 | "@glint/environment-ember-loose": "1.4.1-unstable.b29a807", 43 | "@glint/environment-ember-template-imports": "1.4.1-unstable.b29a807", 44 | "@glint/template": "1.4.0", 45 | "@rollup/plugin-babel": "^6.0.4", 46 | "@tsconfig/ember": "^3.0.8", 47 | "@types/eslint__js": "^8.42.3", 48 | "@types/qunit": "^2.19.10", 49 | "@types/rsvp": "^4.0.9", 50 | "@typescript-eslint/eslint-plugin": "^8.8.1", 51 | "@typescript-eslint/parser": "^8.8.1", 52 | "babel-plugin-ember-template-compilation": "^2.3.0", 53 | "concurrently": "^9.0.1", 54 | "decorator-transforms": "^2.2.2", 55 | "ember-auto-import": "^2.8.1", 56 | "ember-cli": "~5.12.0", 57 | "ember-cli-babel": "^8.2.0", 58 | "ember-cli-htmlbars": "^6.3.0", 59 | "ember-cli-inject-live-reload": "^2.1.0", 60 | "ember-load-initializers": "^3.0.1", 61 | "ember-modifier": "^4.2.0", 62 | "ember-page-title": "^8.2.3", 63 | "ember-qunit": "^8.1.0", 64 | "ember-resolver": "^13.0.2", 65 | "ember-route-template": "^1.0.3", 66 | "ember-source": "~5.12.0", 67 | "ember-source-channel-url": "^3.0.0", 68 | "ember-template-lint": "^6.0.0", 69 | "ember-try": "^3.0.0", 70 | "eslint": "^9.12.0", 71 | "eslint-plugin-ember": "^12.2.1", 72 | "eslint-plugin-n": "^17.10.3", 73 | "eslint-plugin-qunit": "^8.1.2", 74 | "globals": "^15.10.0", 75 | "loader.js": "^4.7.0", 76 | "prettier": "^3.3.3", 77 | "prettier-plugin-ember-template-tag": "^2.0.2", 78 | "qunit": "^2.22.0", 79 | "qunit-dom": "^3.2.1", 80 | "tracked-built-ins": "^3.3.0", 81 | "typescript": "^5.5.4", 82 | "typescript-eslint": "^8.8.1", 83 | "vite": "^5.4.8", 84 | "webpack": "^5.95.0" 85 | }, 86 | "engines": { 87 | "node": ">= 18" 88 | }, 89 | "ember": { 90 | "edition": "octane" 91 | }, 92 | "ember-addon": { 93 | "type": "app", 94 | "version": 2 95 | }, 96 | "exports": { 97 | "./tests/*": "./tests/*", 98 | "./*": "./app/*" 99 | }, 100 | "dependencies": { 101 | "ember-statechart-component": "workspace:*", 102 | "qunit-theme-ember": "^1.0.0", 103 | "xstate": "^5.18.2" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test-app/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import js from '@eslint/js'; 3 | 4 | import ts from 'typescript-eslint'; 5 | 6 | import ember from 'eslint-plugin-ember'; 7 | import emberRecommended from 'eslint-plugin-ember/configs/recommended'; 8 | import gjsRecommended from 'eslint-plugin-ember/configs/recommended-gjs'; 9 | import gtsRecommended from 'eslint-plugin-ember/configs/recommended-gts'; 10 | 11 | import qunit from 'eslint-plugin-qunit'; 12 | import n from 'eslint-plugin-n'; 13 | 14 | import emberParser from 'ember-eslint-parser'; 15 | import babelParser from '@babel/eslint-parser'; 16 | 17 | const parserOptions = { 18 | esm: { 19 | js: { 20 | ecmaFeatures: { modules: true }, 21 | ecmaVersion: 'latest', 22 | }, 23 | ts: { 24 | projectService: true, 25 | tsconfigRootDir: import.meta.dirname, 26 | }, 27 | }, 28 | }; 29 | 30 | export default ts.config( 31 | js.configs.recommended, 32 | { 33 | files: ['**/*.js'], 34 | languageOptions: { 35 | parser: babelParser, 36 | parserOptions: parserOptions.esm.js, 37 | globals: { 38 | ...globals.browser, 39 | }, 40 | }, 41 | plugins: { 42 | ember, 43 | }, 44 | rules: { 45 | ...emberRecommended.rules, 46 | }, 47 | }, 48 | { 49 | files: ['**/*.ts'], 50 | plugins: { ember }, 51 | languageOptions: { 52 | parserOptions: parserOptions.esm.ts, 53 | }, 54 | extends: [...ts.configs.strictTypeChecked, ...emberRecommended], 55 | }, 56 | { 57 | files: ['**/*.gjs'], 58 | languageOptions: { 59 | parser: emberParser, 60 | parserOptions: parserOptions.esm.js, 61 | globals: { 62 | ...globals.browser, 63 | }, 64 | }, 65 | plugins: { 66 | ember, 67 | }, 68 | rules: { 69 | ...emberRecommended.rules, 70 | ...gjsRecommended.rules, 71 | }, 72 | }, 73 | { 74 | files: ['**/*.gts'], 75 | plugins: { ember }, 76 | languageOptions: { 77 | parserOptions: parserOptions.esm.ts, 78 | }, 79 | extends: [ 80 | ...ts.configs.strictTypeChecked, 81 | ...emberRecommended, 82 | ...gtsRecommended, 83 | ], 84 | }, 85 | { 86 | files: ['tests/**/*-test.{js,gjs}'], 87 | plugins: { 88 | qunit, 89 | }, 90 | }, 91 | /** 92 | * CJS node files 93 | */ 94 | { 95 | files: [ 96 | '**/*.cjs', 97 | 'config/**/*.js', 98 | 'testem.js', 99 | 'testem*.js', 100 | '.prettierrc.js', 101 | '.stylelintrc.js', 102 | '.template-lintrc.js', 103 | 'ember-cli-build.js', 104 | ], 105 | plugins: { 106 | n, 107 | }, 108 | 109 | languageOptions: { 110 | sourceType: 'script', 111 | ecmaVersion: 'latest', 112 | globals: { 113 | ...globals.node, 114 | }, 115 | }, 116 | }, 117 | /** 118 | * ESM node files 119 | */ 120 | { 121 | files: ['*.mjs'], 122 | plugins: { 123 | n, 124 | }, 125 | 126 | languageOptions: { 127 | sourceType: 'module', 128 | ecmaVersion: 'latest', 129 | parserOptions: parserOptions.esm.js, 130 | globals: { 131 | ...globals.node, 132 | }, 133 | }, 134 | }, 135 | { 136 | files: ['**/*.{ts,gts}'], 137 | rules: { 138 | '@typescript-eslint/no-unsafe-assignment': 'off', 139 | '@typescript-eslint/no-unsafe-call': 'off', 140 | '@typescript-eslint/no-unsafe-return': 'off', 141 | '@typescript-eslint/no-unsafe-member-access': 'off', 142 | '@typescript-eslint/restrict-plus-operands': 'off', 143 | '@typescript-eslint/no-redundant-type-constituents': 'off', 144 | 'ember/template-no-let-reference': 'off', 145 | }, 146 | }, 147 | /** 148 | * Settings 149 | */ 150 | { 151 | ignores: ['dist/', 'node_modules/', 'coverage/', '!**/.*'], 152 | linterOptions: { 153 | reportUnusedDisableDirectives: 'error', 154 | }, 155 | }, 156 | ); 157 | -------------------------------------------------------------------------------- /ember-statechart-component/src/-private/statechart-manager.js: -------------------------------------------------------------------------------- 1 | import { tracked } from '@glimmer/tracking'; 2 | import { capabilities } from '@ember/component'; 3 | import { assert } from '@ember/debug'; 4 | import { associateDestroyableChild, destroy, isDestroyed, isDestroying } from '@ember/destroyable'; 5 | import { getOwner, setOwner } from '@ember/owner'; 6 | import { cancel, later } from '@ember/runloop'; 7 | 8 | import { createActor } from 'xstate'; 9 | 10 | export const UPDATE_EVENT_NAME = 'EXTERNAL_UPDATE'; 11 | 12 | const STOP = Symbol.for('__ESM__STOP__'); 13 | const START = Symbol.for('__ESM__START__'); 14 | const HANDLE_UPDATE = Symbol.for('__ESM__HANDLE_UPDATE__'); 15 | 16 | /** 17 | * https://stately.ai/docs/migration#the-statetostrings-method-has-been-removed 18 | */ 19 | function getStateValueStrings(stateValue) { 20 | if (typeof stateValue === 'string') { 21 | return [stateValue]; 22 | } 23 | 24 | const valueKeys = Object.keys(stateValue); 25 | 26 | return valueKeys.concat( 27 | ...valueKeys.map((key) => getStateValueStrings(stateValue[key]).map((s) => key + '.' + s)) 28 | ); 29 | } 30 | 31 | class ReactiveActor { 32 | /** 33 | * @private 34 | */ 35 | @tracked lastSnapshot; 36 | 37 | #actor; 38 | #callbacks = []; 39 | 40 | /** 41 | * The most recent snapshot available from the actor 42 | */ 43 | get snapshot() { 44 | return this.lastSnapshot; 45 | } 46 | 47 | /** 48 | * Alias fort the value of the snapshot 49 | */ 50 | get value() { 51 | return this.snapshot?.value; 52 | } 53 | 54 | /** 55 | * @type {import('xstate').Snapshot['matches']} 56 | */ 57 | matches = (...args) => this.snapshot.matches(...args); 58 | 59 | /** 60 | * The dot-separated name of the current state. 61 | * If multiple states are active, those well be comma-separated 62 | */ 63 | get statePath() { 64 | let x = this.value; 65 | 66 | if (typeof x === 'string') { 67 | return x; 68 | } 69 | 70 | if (typeof x === 'object') { 71 | if (!x) return ''; 72 | 73 | return getStateValueStrings(x).join(','); 74 | } 75 | 76 | return `${x}`; 77 | } 78 | 79 | /** 80 | * The running actor, directly from XState 81 | */ 82 | get actor() { 83 | return this.#actor; 84 | } 85 | 86 | constructor(actor, owner) { 87 | this.#actor = actor; 88 | setOwner(this, owner); 89 | 90 | let initialSnapshot = actor.getSnapshot(); 91 | let context = initialSnapshot.context; 92 | 93 | assert( 94 | `Machine init failed with error: ${initialSnapshot?.error?.message}`, 95 | !initialSnapshot?.error?.message 96 | ); 97 | 98 | // Required for services to be accessible 99 | setOwner(context, owner); 100 | 101 | this.lastSnapshot = initialSnapshot; 102 | 103 | actor.subscribe(async (snapshot) => { 104 | await Promise.resolve(); 105 | 106 | if (this.lastSnapshot === snapshot) return; 107 | 108 | if (isDestroyed(actor) || isDestroying(actor)) { 109 | return; 110 | } 111 | 112 | this.lastSnapshot = snapshot; 113 | this.#callbacks.forEach((fn) => fn(snapshot)); 114 | }); 115 | } 116 | 117 | /** 118 | * Forwards to the underlying actor.send, 119 | * but re-enables passing vanilla strings as events, 120 | * rather than needing to create an object for each event. 121 | * (XState docs recommend send({ type: EVENT_NAME })) 122 | */ 123 | send = (...args) => { 124 | if (typeof args[0] === 'string') { 125 | this.#actor.send({ type: args[0] }); 126 | 127 | return; 128 | } 129 | 130 | this.#actor.send(...args); 131 | }; 132 | 133 | /** 134 | * subscribe to changes in the machine state 135 | */ 136 | onTransition = (callback) => { 137 | this.#callbacks.push(callback); 138 | }; 139 | 140 | [START] = () => this.#actor.start(); 141 | [STOP] = () => this.#actor.stop(); 142 | [HANDLE_UPDATE] = (args) => { 143 | if (Object.keys(args.named).length > 0 || args.positional.length > 0) { 144 | this.send(UPDATE_EVENT_NAME, args.named); 145 | } 146 | }; 147 | } 148 | 149 | export default class ComponentManager { 150 | capabilities = capabilities('3.13', { 151 | destructor: true, 152 | updateHook: true, 153 | }); 154 | 155 | static create(owner) { 156 | let manager = new ComponentManager(); 157 | 158 | setOwner(manager, owner); 159 | 160 | return manager; 161 | } 162 | 163 | createComponent(machine, args) { 164 | let { named } = args; 165 | 166 | if ('config' in named) { 167 | machine = machine.provide(named['config']); 168 | } 169 | 170 | let options = { 171 | context: {}, 172 | clock: { 173 | setTimeout(fn, ms) { 174 | return later.call(null, fn, ms); 175 | }, 176 | clearTimeout(timer) { 177 | return cancel.call(null, timer); 178 | }, 179 | }, 180 | }; 181 | 182 | if ('input' in named) { 183 | options.input = named['input']; 184 | } 185 | 186 | if ('context' in named) { 187 | options.context = named['context']; 188 | } 189 | 190 | if ('snapshot' in named) { 191 | options.snapshot = named['snapshot']; 192 | } 193 | 194 | let owner = getOwner(this); 195 | 196 | setOwner(options.context, owner); 197 | 198 | let actor = createActor(machine, options); 199 | let state = new ReactiveActor(actor, owner); 200 | 201 | associateDestroyableChild(actor, this); 202 | 203 | state[START](); 204 | 205 | return state; 206 | } 207 | 208 | /** 209 | * updateComponent is called every time context changes, because context is tracked data 210 | * and is read during `createComponent`. 211 | * 212 | * This is why we neeed to guard this UPDATE_EVENT_NAME around a condition about if there 213 | * even are args. 214 | * 215 | * This isn't a perfect solution, but for the common use case of 216 | * "handle everything within the statechart and don't pass args", 217 | * it should be good enough. 218 | */ 219 | updateComponent(state, args) { 220 | state[HANDLE_UPDATE](args); 221 | } 222 | 223 | destroyComponent(state) { 224 | if (isDestroying(state)) { 225 | return; 226 | } 227 | 228 | state[STOP](); 229 | 230 | destroy(state); 231 | } 232 | 233 | getContext(state) { 234 | return state; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /ember-statechart-component/src/public-types.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | 3 | /** 4 | * Public API is defined by this file. 5 | * If you use any API outside of what is defined here, 6 | * it's possible it won't be supported. 7 | */ 8 | 9 | import type { Registry } from '@ember/service'; 10 | 11 | export function getService( 12 | context: unknown, 13 | serviceName: Key 14 | ): Registry[Key]; 15 | 16 | import type { ComponentLike } from '@glint/template'; 17 | import type { AnyActorLogic, ContextFrom, EventFromLogic, TagsFrom, ToChildren } from 'xstate'; 18 | import type { 19 | Actor, 20 | ActorLogic, 21 | ActorSystem, 22 | AnyActorRef, 23 | EventObject, 24 | MachineContext, 25 | MachineSnapshot, 26 | MetaObject, 27 | ParameterizedObject, 28 | ProvidedActor, 29 | // StateMachine must be imported 30 | // in order for the interface to merge 31 | StateMachine, 32 | StateSchema, 33 | StateValue, 34 | // StateValueMap must be imported 35 | // in order for the interface to merge 36 | StateValueMap, 37 | } from 'xstate'; 38 | 39 | /** 40 | * NOTE: before you think about making StateValue renderable, 41 | * remember that it can be an object (for nested states) 42 | */ 43 | function send(eventType: string): void; 44 | function send< 45 | TContext, 46 | TEvent, 47 | TChildren, 48 | TActor, 49 | TAction, 50 | TGuard, 51 | TDelay, 52 | TStateValue, 53 | TTag, 54 | TInput, 55 | TOutput, 56 | TEmitted, 57 | TMeta, 58 | TConfig, 59 | >( 60 | event: EventFromLogic< 61 | StateMachine< 62 | TContext, 63 | TEvent, 64 | TChildren, 65 | TActor, 66 | TAction, 67 | TGuard, 68 | TDelay, 69 | TStateValue, 70 | TTag, 71 | TInput, 72 | TOutput, 73 | TEmitted, 74 | TMeta, 75 | TConfig 76 | > 77 | > 78 | ): void; 79 | 80 | export type ReactiveActorFrom = Signature< 81 | ContextFrom, // Context 82 | EventFrom, // Event 83 | ToChildren, // Children, 84 | any, // ProvidedActor 85 | any, // Action, 86 | any, // Guard, 87 | any, // Delay, 88 | StateValueFrom, 89 | TagsFrom, 90 | any, // Input, 91 | any, // Output, 92 | EventObject, // Emitted, 93 | MetaObject, // Meta, 94 | StateSchema // Config, 95 | >['Blocks']['default'][0]; 96 | 97 | interface Signature< 98 | TContext extends MachineContext, 99 | TEvent extends EventObject, 100 | TChildren extends Record, 101 | TActor extends ProvidedActor, 102 | TAction extends ParameterizedObject, 103 | TGuard extends ParameterizedObject, 104 | TDelay extends string, 105 | TStateValue extends StateValue, 106 | TTag extends string, 107 | TInput, 108 | TOutput, 109 | TEmitted extends EventObject, 110 | TMeta extends MetaObject, 111 | TConfig extends StateSchema, 112 | > { 113 | Args: { 114 | config?: TConfig; 115 | context?: TContext; 116 | input?: TInput; 117 | snapshot?: TStateValue; 118 | }; 119 | Blocks: { 120 | default: [ 121 | machine: { 122 | /** 123 | * Pass a function to this to be called when the machine transitions state 124 | */ 125 | onTransition: ( 126 | ...params: Parameters< 127 | Actor< 128 | StateMachine< 129 | TContext, 130 | TEvent, 131 | TChildren, 132 | TActor, 133 | TAction, 134 | TGuard, 135 | TDelay, 136 | TStateValue, 137 | TTag, 138 | TInput, 139 | TOutput, 140 | TEmitted, 141 | TMeta, 142 | TConfig 143 | > 144 | >['subscribe'] 145 | > 146 | ) => void; 147 | /** 148 | * The current snapshot's value. 149 | * Will be a string, or object, depending on state complexity. 150 | */ 151 | value: StateValue; 152 | /** 153 | * The snapshot value (state), as a dot-separated string 154 | */ 155 | statePath: string; 156 | /** 157 | * Send an event to the machine. 158 | * For simple events, passing only a string is allowed as an alias 159 | * for { type: "EVENT_NAME" } 160 | */ 161 | send: typeof send; 162 | 163 | /** 164 | * Alias for snapshot.matches, 165 | * returns true of the passed state path is active. 166 | */ 167 | matches: (statePath: string) => boolean; 168 | 169 | /** 170 | * The Machine's Snapshot 171 | */ 172 | snapshot: SnapshotFrom< 173 | StateMachine< 174 | TContext, 175 | TEvent, 176 | TChildren, 177 | TActor, 178 | TAction, 179 | TGuard, 180 | TDelay, 181 | TStateValue, 182 | TTag, 183 | TInput, 184 | TOutput, 185 | TEmitted, 186 | TMeta, 187 | TConfig 188 | > 189 | >; 190 | /** 191 | * If specific behavior is needed, or for more type-accurate 192 | * usage in templates, the full actor is exposed here. 193 | */ 194 | actor: Actor< 195 | StateMachine< 196 | TContext, 197 | TEvent, 198 | TChildren, 199 | TActor, 200 | TAction, 201 | TGuard, 202 | TDelay, 203 | TStateValue, 204 | TTag, 205 | TInput, 206 | TOutput, 207 | TEmitted, 208 | TMeta, 209 | TConfig 210 | > 211 | >; 212 | }, 213 | ]; 214 | }; 215 | } 216 | 217 | declare module 'xstate' { 218 | // Mixing in ComponentLike to the StateMachine type 219 | interface StateMachine< 220 | TContext extends MachineContext, 221 | TEvent extends EventObject, 222 | TChildren extends Record, 223 | TActor extends ProvidedActor, 224 | TAction extends ParameterizedObject, 225 | TGuard extends ParameterizedObject, 226 | TDelay extends string, 227 | TStateValue extends StateValue, 228 | TTag extends string, 229 | TInput, 230 | TOutput, 231 | TEmitted extends EventObject, 232 | TMeta extends MetaObject, 233 | TConfig extends StateSchema, 234 | > extends ComponentLike, 235 | ActorLogic< 236 | MachineSnapshot, 237 | TEvent, 238 | TInput, 239 | ActorSystem, 240 | TEmitted 241 | > { 242 | // Indicator that we've messed with the types comeing from xstate. 243 | // (Also useful for debugging) 244 | __has_been_declaration_merged_from_ember_statechart_component__: string; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 |
4 | 5 | 6 | XState logotype 7 | 8 |
9 | Actor-based state-management as components. → Documentation 10 |
11 |
12 |

13 | 14 | 15 | [![CI](https://github.com/NullVoxPopuli/ember-statechart-component/actions/workflows/ci.yml/badge.svg)](https://github.com/NullVoxPopuli/ember-statechart-component/actions/workflows/ci.yml) 16 | [![npm version](https://badge.fury.io/js/ember-statechart-component.svg)](https://www.npmjs.com/package/ember-statechart-component) 17 | 18 | 19 | Support 20 | ------------------------------------------------------------------------------ 21 | 22 | - XState >= 5 23 | - TypeScript >= 5.2 24 | - ember-source >= 5.1 25 | - Glint >= 1.2.1 26 | 27 | Installation 28 | ------------------------------------------------------------------------------ 29 | 30 | ```bash 31 | npm install ember-statechart-component 32 | ``` 33 | 34 | Anywhere in your app, though, if you don't have anywhere specific in mind, the `app/app.js` will be just fine: 35 | 36 | ```ts 37 | import 'ember-statechart-component'; 38 | ``` 39 | 40 | This instructs Ember how to render and create actors from state machines. 41 | 42 | ## Migrating from XState v4? 43 | 44 | See: https://stately.ai/docs/migration 45 | 46 | 47 | Usage 48 | ------------------------------------------------------------------------------ 49 | 50 | ```gjs 51 | import { createMachine } from 'xstate'; 52 | 53 | const Toggler = createMachine({ 54 | initial: 'inactive', 55 | states: { 56 | inactive: { on: { TOGGLE: 'active' } }, 57 | active: { on: { TOGGLE: 'inactive' } }, 58 | }, 59 | }); 60 | 61 | 70 | ``` 71 | 72 | ### Accessing Ember Services 73 | 74 | ```gjs 75 | import { getService } from 'ember-statechart-component'; 76 | import { setup } from 'xstate'; 77 | 78 | const AuthenticatedToggle = setup({ 79 | actions: { 80 | notify: ({ context }) => { 81 | getService(context, 'toasts').notify('You must be logged in'); 82 | }, 83 | }, 84 | guards: { 85 | isAuthenticated: ({ context }) => getService(context, 'session').isAuthenticated, 86 | }, 87 | }).createMachine({ 88 | initial: 'inactive', 89 | states: { 90 | inactive: { 91 | on: { 92 | TOGGLE: [ 93 | { 94 | target: 'active', 95 | guard: 'isAuthenticated', 96 | }, 97 | { actions: ['notify'] }, 98 | ], 99 | }, 100 | }, 101 | active: { on: { TOGGLE: 'inactive' } }, 102 | }, 103 | }); 104 | 105 | 114 | ``` 115 | 116 | 117 | ### Matching States 118 | 119 | ```hbs 120 | 121 | {{#if (toggle.matches 'inactive')}} 122 | The inactive state 123 | {{else if (toggle.matches 'active')}} 124 | The active state 125 | {{else}} 126 | Unknown state 127 | {{/if}} 128 | 129 | 132 | 133 | ``` 134 | 135 | ### API 136 | 137 | 138 | #### `@config` 139 | 140 | This argument allows you to pass a [MachineOptions](https://xstate.js.org/docs/packages/xstate-fsm/#api) for [actions](https://xstate.js.org/docs/guides/actions.html), [services](https://xstate.js.org/docs/guides/communication.html#configuring-services), [guards](https://xstate.js.org/docs/guides/guards.html#serializing-guards), etc. 141 | 142 | Usage: 143 | 144 |
Toggle machine that needs a config 145 | 146 | ```js 147 | // app/components/toggle.js 148 | import { createMachine, assign } from 'xstate'; 149 | 150 | export default createMachine({ 151 | initial: 'inactive', 152 | states: { 153 | inactive: { on: { TOGGLE: 'active' } }, 154 | active: { 155 | on: { 156 | TOGGLE: { 157 | target: 'inactive', 158 | actions: ['toggleIsOn'] 159 | } 160 | } 161 | }, 162 | }, 163 | }); 164 | ``` 165 | 166 |
167 | 168 | ```hbs 169 | 176 | 179 | 180 | ``` 181 | 182 | #### `@input` 183 | 184 | Providing inputs from arguments works as you expect, following docs from [XState: Input](https://stately.ai/docs/input) 185 | 186 | ```glimmer-ts 187 | const Toggle = createMachine({ 188 | types: { 189 | input: {} as { numCalled?: number }, 190 | }, 191 | initial: 'inactive', 192 | context: ({ input }) => { 193 | return { 194 | numCalled: input.numCalled ?? 0, 195 | }; 196 | }, 197 | states: { 198 | inactive: { 199 | entry: assign({ 200 | numCalled: ({ context }) => context.numCalled + 1, 201 | }), 202 | on: { TOGGLE: 'active' }, 203 | }, 204 | active: { 205 | entry: assign({ 206 | numCalled: ({ context }) => context.numCalled + 1, 207 | }), 208 | on: { TOGGLE: 'inactive' }, 209 | }, 210 | }, 211 | }); 212 | 213 | const input = { 214 | numCalled: 10, 215 | }; 216 | 217 | 226 | ``` 227 | 228 | #### `@context` 229 | 230 | Sets the initial context. The current value of the context can then be accessed via `state.context`. 231 | 232 | Usage: 233 | 234 |
Toggle machine that interacts with context 235 | 236 | ```js 237 | import { createMachine, assign } from 'xstate'; 238 | 239 | export default createMachine({ 240 | initial: 'inactive', 241 | states: { 242 | inactive: { 243 | on: { 244 | TOGGLE: { 245 | target: 'active', 246 | actions: ['increaseCounter'] 247 | } 248 | } 249 | }, 250 | active: { 251 | on: { 252 | TOGGLE: { 253 | target: 'inactive', 254 | actions: ['increaseCounter'] 255 | } 256 | } 257 | }, 258 | }, 259 | }, { 260 | actions: { 261 | increaseCounter: assign({ 262 | counter: (context) => context.counter + 1 263 | }) 264 | } 265 | }); 266 | ``` 267 | 268 |
269 | 270 | ```hbs 271 | 272 | 275 | 276 |

277 | Toggled: {{toggle.snapshot.context.counter}} times. 278 |

279 |
280 | ``` 281 | 282 | #### `@snapshot` 283 | 284 | The machine will use `@snapshot` as the initial state. 285 | Any changes to this argument 286 | are not automatically propagated to the machine. 287 | An update event (see details below) is sent instead. 288 | 289 | ### What happens if any of the passed args change? 290 | 291 | An event will be sent to the machine for you, along 292 | with all named arguments used to invoke the component. 293 | 294 | To work with this event, use the constant provided by this library: 295 | 296 | ```js 297 | 298 | import { UPDATE_EVENT_NAME } from 'ember-statechart-component'; 299 | 300 | 301 | const MyMachine = createMachine({ 302 | initial: 'inactive', 303 | states: { 304 | [UPDATE_EVENT_NAME]: { /* ... */ 305 | /* ... */ 306 | }, 307 | }); 308 | ``` 309 | 310 | The value of this constant is just `EXTERNAL_UPDATE`, but the import makes it clear _why_ it exists, as the name does need to exactly match how the ember component manager is implemented for machines. 311 | 312 | 313 | ### API 314 | 315 | The yielded value from an invoked state machine has some properties on it as well as the actor that allows you to "just defer to XState" for most situations. 316 | 317 | Given this a machine and its invocation, 318 | ```gjs 319 | import { createMachine } from 'xstate'; 320 | 321 | const Authenticator = createMachine({ /* ... */ }); 322 | 323 | 329 | ``` 330 | 331 | - `actor` - The underlying actor that XState manages, see: [The Actor Docs](https://stately.ai/docs/category/actors) 332 | - `snapshot` - The most recent snapshot available from the actor 333 | - `value` - alias for `snapshot.value`, which represents the name of the state, or an array of states, if the current state is nested. 334 | - `statePath` - a dot-separated string representing the current `value` 335 | - `matches` - The [matches function](https://stately.ai/docs/states#statematchesstatevalue) 336 | - `onTransition` - A way to arbitrarily run code when the machine transitions. 337 | 338 | 339 | 340 | Contributing 341 | ------------------------------------------------------------------------------ 342 | 343 | See the [Contributing](CONTRIBUTING.md) guide for details. 344 | 345 | 346 | License 347 | ------------------------------------------------------------------------------ 348 | 349 | This project is licensed under the [MIT License](LICENSE.md). 350 | -------------------------------------------------------------------------------- /test-app/tests/integration/dynamic-machines-test.gts: -------------------------------------------------------------------------------- 1 | // /** eslint-disable @typescript-eslint/no-explicit-any */ 2 | // import { clearRender, click, render } from '@ember/test-helpers'; 3 | // import { module, test } from 'qunit'; 4 | // import { setupRenderingTest } from 'ember-qunit'; 5 | // import { on } from '@ember/modifier'; 6 | // import { fn } from '@ember/helper'; 7 | // 8 | // import { assign, createMachine, setup } from 'xstate'; 9 | // 10 | // // Pending fix in glimmer-vm 11 | // // state.matches *should* just work 12 | // const call = (obj, fun, ...args) => fun.call(obj, ...args); 13 | // 14 | // module('Dynamic Machines', function (hooks) { 15 | // setupRenderingTest(hooks); 16 | // 17 | // // module('xstate#spawn', function () { 18 | // // test('Can have a dynamically created machine and is reactive', async function (assert) { 19 | // // function createNested(context: Record, config: any = {}) { 20 | // // return createMachine({ 21 | // // id: 'nested', 22 | // // initial: 'inactive', 23 | // // 24 | // // context: { 25 | // // inactive: 0, 26 | // // active: 0, 27 | // // ...context, 28 | // // }, 29 | // // 30 | // // states: { 31 | // // inactive: { 32 | // // entry: ['incrementInactive'], 33 | // // on: { TOGGLE: 'active' }, 34 | // // }, 35 | // // active: { 36 | // // entry: ['incrementActive'], 37 | // // on: { TOGGLE: 'inactive' }, 38 | // // }, 39 | // // }, 40 | // // }).withConfig(config); 41 | // // } 42 | // // 43 | // // const parentMachine = createMachine({ 44 | // // id: 'parent', 45 | // // initial: 'idle', 46 | // // 47 | // // states: { 48 | // // idle: { 49 | // // on: { 50 | // // SPAWN: 'spawnNested', 51 | // // }, 52 | // // }, 53 | // // spawnNested: { 54 | // // entry: assign({ 55 | // // someRef: (_context, { nested, id }: any) => { 56 | // // return spawn(nested, id); 57 | // // }, 58 | // // }), 59 | // // }, 60 | // // }, 61 | // // }); 62 | // // 63 | // // this.owner.register('component:test-machine', parentMachine); 64 | // // 65 | // // let active = 0; 66 | // // let inactive = 0; 67 | // // 68 | // // this.setProperties({ 69 | // // startNested: (send: Send) => 70 | // // send('SPAWN', { 71 | // // id: 'named-spawned-machine', 72 | // // nested: createNested( 73 | // // { active: 3 }, 74 | // // { 75 | // // actions: { 76 | // // incrementActive: () => active++, 77 | // // incrementInactive: () => inactive++, 78 | // // }, 79 | // // } 80 | // // ), 81 | // // }), 82 | // // }); 83 | // // 84 | // // await render(hbs` 85 | // // 86 | // // {{to-string state.value}} 87 | // // 88 | // // 89 | // // 90 | // // {{#if (to-any state 'context.someRef')}} 91 | // // {{!-- 92 | // // someRef.state does not have a reactive wrapper, like the root interpreter does 93 | // // 94 | // // TODO: make this reactive 95 | // // --}} 96 | // // {{to-any state 'context.someRef.state.value'}} 97 | // // 98 | // // 99 | // // {{/if}} 100 | // // 101 | // // 102 | // // `); 103 | // // 104 | // // assert.dom().containsText('idle'); 105 | // // assert.strictEqual(active, 0); 106 | // // assert.strictEqual(inactive, 0); 107 | // // 108 | // // await click('#spawn'); 109 | // // assert.strictEqual(active, 0); 110 | // // assert.strictEqual(inactive, 1); 111 | // // 112 | // // assert.dom().containsText('spawnNested'); 113 | // // 114 | // // /** 115 | // // * There is not a way to easily access spawned machines 116 | // // * internal state, so we've wired up some actions / callbacks 117 | // // * 118 | // // * (the way to access the nested state is state.children[machine-id].state) 119 | // // */ 120 | // // await click('#toggle'); 121 | // // 122 | // // assert.strictEqual(active, 1); 123 | // // assert.strictEqual(inactive, 1); 124 | // // 125 | // // await click('#toggle'); 126 | // // 127 | // // assert.strictEqual(active, 1); 128 | // // assert.strictEqual(inactive, 2); 129 | // // }); 130 | // // }); 131 | // 132 | // module('xstate#invoke', function () { 133 | // test('it works with invoked machines', async function (assert) { 134 | // // Invoked child machine 135 | // const minuteMachine = createMachine({ 136 | // initial: 'active', 137 | // states: { 138 | // active: { 139 | // on: { 140 | // DECLARE_DONE: 'finished', 141 | // }, 142 | // }, 143 | // finished: { type: 'final' }, 144 | // }, 145 | // }); 146 | // 147 | // const ParentMachine = createMachine({ 148 | // id: 'parent', 149 | // initial: 'pending', 150 | // states: { 151 | // pending: { 152 | // invoke: { 153 | // id: 'timer', 154 | // src: minuteMachine, 155 | // onDone: 'timesUp', 156 | // }, 157 | // }, 158 | // timesUp: { 159 | // type: 'final', 160 | // }, 161 | // }, 162 | // }); 163 | // 164 | // const declareDone = { type: 'DECLARE_DONE' }; 165 | // const value = (x: any) => x?.getSnapshot?.()?.value; 166 | // 167 | // await render( 168 | // 178 | // ); 179 | // 180 | // assert.dom('#parent').containsText('pending'); 181 | // assert.dom('#child').containsText('active'); 182 | // 183 | // await click('button'); 184 | // 185 | // assert.dom('#parent').containsText('timesUp'); 186 | // assert.dom('#child').hasNoText('the machine destroyed itself (on purpose)'); 187 | // }); 188 | // 189 | // test('it works with the child machine updating its own context', async function (assert) { 190 | // const childMachine = setup({ 191 | // actions: { 192 | // updateContext: assign({ 193 | // prop1: () => { 194 | // return 'new value'; 195 | // }, 196 | // }), 197 | // }, 198 | // }).createMachine({ 199 | // id: 'child-machine', 200 | // initial: 'idleChild', 201 | // context: ({ input }) => { return input }, 202 | // states: { 203 | // idleChild: { 204 | // entry: () => { assert.step('entry: child.idleChild'); }, 205 | // exit: () => { assert.step('exit: child.idleChild'); }, 206 | // on: { 207 | // UPDATE_CONTEXT: { actions: ['updateContext'] }, 208 | // }, 209 | // }, 210 | // }, 211 | // }, 212 | // ); 213 | // 214 | // const parentMachine = createMachine({ 215 | // id: 'parent-state-machine', 216 | // initial: 'idle', 217 | // states: { 218 | // idle: { 219 | // entry: () => { assert.step('entry: parent.idle'); }, 220 | // exit: () => { assert.step('exit: parent.idle'); }, 221 | // on: { INVOKE_CHILD: 'withChildMachine' }, 222 | // }, 223 | // withChildMachine: { 224 | // entry: () => { assert.step('entry: parent.withChildMachine'); }, 225 | // exit: () => { assert.step('exit: parent.withChildMachine'); }, 226 | // invoke: { 227 | // id: 'child-machine', 228 | // src: childMachine, 229 | // input: () => ({ prop1: 'original value', }), 230 | // }, 231 | // on: { 232 | // RESET: 'idle', 233 | // }, 234 | // }, 235 | // }, 236 | // }); 237 | // 238 | // const invokeChild = { type: 'INVOKE_CHILD' }; 239 | // const reset = { type: 'RESET' }; 240 | // const updateContext = { type: 'UPDATE_CONTEXT' }; 241 | // const prop = (x) => x?.children?.['child-machine']?.getSnapshot().context.prop1; 242 | // const update = (x) => x.children['child-machine'].send(updateContext); 243 | // const snapshot = (x) => x.children['child-machine']?.getSnapshot(); 244 | // const getValue = (x) => x.value; 245 | // const getChild = (x) => x.snapshot?.children['child-machine']; 246 | // 247 | // await render( 248 | // 270 | // ); 271 | // 272 | // await click('#invoke-child'); 273 | // 274 | // assert.dom('out').containsText('original value'); 275 | // 276 | // await click('#update-context'); 277 | // 278 | // assert.dom('out').containsText('new value'); 279 | // 280 | // assert.verifySteps([ 281 | // 'entry: parent.idle', 282 | // 'exit: parent.idle', 283 | // 'entry: parent.withChildMachine', 284 | // 'entry: child.idleChild', 285 | // ]); 286 | // 287 | // await clearRender(); 288 | // 289 | // assert.verifySteps(['exit: parent.withChildMachine', 'exit: child.idleChild']); 290 | // }); 291 | // }); 292 | // }); 293 | -------------------------------------------------------------------------------- /test-app/tests/integration/usage-test.gts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | import { fn } from '@ember/helper'; 4 | import { on } from '@ember/modifier'; 5 | import Service from '@ember/service'; 6 | import { clearRender, render } from '@ember/test-helpers'; 7 | import click from '@ember/test-helpers/dom/click'; 8 | import { module, test } from 'qunit'; 9 | import { setupRenderingTest } from 'ember-qunit'; 10 | 11 | import { getService } from 'ember-statechart-component'; 12 | import { assign, createMachine } from 'xstate'; 13 | 14 | declare module '@ember/service' { 15 | interface Registry { 16 | 'test-state': { 17 | foo: number; 18 | }; // determined in tests 19 | } 20 | } 21 | 22 | // Pending fix in glimmer-vm 23 | // state.matches *should* just work 24 | // @ts-expect-error todo: don't use call at all 25 | const call = (obj, fun, ...args) => fun.call(obj, ...args); 26 | 27 | /** 28 | * any casting will be fixed when tests can be gts 29 | */ 30 | module('Usage', function (hooks) { 31 | setupRenderingTest(hooks); 32 | 33 | test('it works', async function (assert) { 34 | const Toggle = createMachine({ 35 | initial: 'inactive', 36 | states: { 37 | inactive: { on: { TOGGLE: 'active' } }, 38 | active: { on: { TOGGLE: 'inactive' } }, 39 | }, 40 | }); 41 | 42 | await render( 43 | 52 | ); 53 | 54 | assert.dom().containsText('inactive'); 55 | 56 | await click('button'); 57 | 58 | assert.dom().doesNotContainText('inactive'); 59 | assert.dom().containsText('active'); 60 | }); 61 | 62 | test('can use services', async function (assert) { 63 | const Toggle = createMachine( 64 | { 65 | initial: 'inactive', 66 | states: { 67 | inactive: { entry: 'increment', on: { TOGGLE: 'active' } }, 68 | active: { entry: 'increment', on: { TOGGLE: 'inactive' } }, 69 | }, 70 | }, 71 | { 72 | actions: { 73 | increment: ({ context }) => { 74 | getService(context, 'test-state').foo++; 75 | }, 76 | }, 77 | } 78 | ); 79 | 80 | this.owner.register( 81 | 'service:test-state', 82 | class TestState extends Service { 83 | foo = 0; 84 | } 85 | ); 86 | 87 | await render( 88 | 97 | ); 98 | 99 | const testState = this.owner.lookup('service:test-state') as { 100 | foo: number; 101 | }; 102 | 103 | assert.strictEqual(testState.foo, 1); 104 | 105 | await click('button'); 106 | assert.strictEqual(testState.foo, 2); 107 | 108 | await click('button'); 109 | assert.strictEqual(testState.foo, 3); 110 | }); 111 | 112 | test(`it can use XState's builtin matches function`, async function (assert) { 113 | const Toggle = createMachine({ 114 | initial: 'inactive', 115 | states: { 116 | inactive: { on: { TOGGLE: 'active' } }, 117 | active: { on: { TOGGLE: 'inactive' } }, 118 | }, 119 | }); 120 | 121 | await render( 122 | 137 | ); 138 | 139 | assert.dom().containsText('The inactive state'); 140 | 141 | await click('button'); 142 | 143 | assert.dom().containsText('The active state'); 144 | }); 145 | 146 | test(`it can use the wrapped matches function`, async function (assert) { 147 | const Toggle = createMachine({ 148 | initial: 'inactive', 149 | states: { 150 | inactive: { on: { TOGGLE: 'active' } }, 151 | active: { on: { TOGGLE: 'inactive' } }, 152 | }, 153 | }); 154 | 155 | await render( 156 | 171 | ); 172 | 173 | assert.dom().containsText('The inactive state'); 174 | 175 | await click('button'); 176 | 177 | assert.dom().containsText('The active state'); 178 | }); 179 | 180 | test('multiple invocations have their own state', async function (assert) { 181 | const Toggle = createMachine({ 182 | initial: 'inactive', 183 | states: { 184 | inactive: { on: { TOGGLE: 'active' } }, 185 | active: { on: { TOGGLE: 'inactive' } }, 186 | }, 187 | }); 188 | 189 | await render( 190 | 210 | ); 211 | 212 | assert.dom('#one').containsText('inactive'); 213 | assert.dom('#two').containsText('inactive'); 214 | 215 | await click('#one button'); 216 | 217 | assert.dom('#one').doesNotContainText('inactive'); 218 | assert.dom('#one').containsText('active'); 219 | assert.dom('#two').containsText('inactive'); 220 | }); 221 | 222 | test('can pass config', async function (assert) { 223 | const Toggle = createMachine({ 224 | initial: 'inactive', 225 | states: { 226 | inactive: { entry: 'increment', on: { TOGGLE: 'active' } }, 227 | active: { entry: 'increment', on: { TOGGLE: 'inactive' } }, 228 | }, 229 | }); 230 | 231 | let numCalled = 0; 232 | 233 | const config = { 234 | actions: { 235 | increment: () => numCalled++, 236 | }, 237 | }; 238 | 239 | await render( 240 | 247 | ); 248 | 249 | assert.strictEqual(numCalled, 1); 250 | 251 | await click('button'); 252 | assert.strictEqual(numCalled, 2); 253 | 254 | await click('button'); 255 | assert.strictEqual(numCalled, 3); 256 | }); 257 | 258 | test('can pass context', async function (assert) { 259 | const Toggle = createMachine({ 260 | types: { 261 | input: {} as { numCalled?: number }, 262 | }, 263 | initial: 'inactive', 264 | context: ({ input }) => { 265 | return { 266 | numCalled: input.numCalled ?? 0, 267 | }; 268 | }, 269 | states: { 270 | inactive: { 271 | entry: assign({ 272 | numCalled: ({ context }) => context.numCalled + 1, 273 | }), 274 | on: { TOGGLE: 'active' }, 275 | }, 276 | active: { 277 | entry: assign({ 278 | numCalled: ({ context }) => context.numCalled + 1, 279 | }), 280 | on: { TOGGLE: 'inactive' }, 281 | }, 282 | }, 283 | }); 284 | 285 | const input = { 286 | numCalled: 10, 287 | }; 288 | 289 | let context = { numCalled: null }; 290 | 291 | const report = (data: any) => (context = data); 292 | 293 | await render( 294 | 303 | ); 304 | 305 | assert.strictEqual(context.numCalled, 11); 306 | 307 | await click('button'); 308 | assert.strictEqual(context.numCalled, 12); 309 | 310 | await click('button'); 311 | assert.strictEqual(context.numCalled, 13); 312 | }); 313 | 314 | test('merging passed context by default', async function (assert) { 315 | const Toggle = createMachine({ 316 | initial: 'inactive', 317 | context: ({ input }) => Object.assign({ foo: 'foo' }, input), 318 | states: { 319 | inactive: { on: { TOGGLE: 'active' } }, 320 | active: { on: { TOGGLE: 'inactive' } }, 321 | }, 322 | }); 323 | 324 | const input = { bar: 'bar' }; 325 | 326 | await render( 327 | 333 | ); 334 | 335 | assert.dom().containsText('foo, bar'); 336 | }); 337 | 338 | test('can pass initial state', async function (assert) { 339 | const Toggle = createMachine({ 340 | initial: 'inactive', 341 | states: { 342 | inactive: { on: { TOGGLE: 'active' } }, 343 | active: { on: { TOGGLE: 'inactive' } }, 344 | }, 345 | }); 346 | 347 | let previousState: any | null = null; 348 | 349 | const report = (state: any) => (previousState = state); 350 | 351 | await render( 352 | 362 | ); 363 | 364 | assert.dom().containsText('inactive'); 365 | 366 | await click('button'); 367 | 368 | assert.dom().doesNotContainText('inactive'); 369 | assert.dom().containsText('active'); 370 | 371 | assert.ok(previousState, 'previous state has been captured'); 372 | 373 | await clearRender(); 374 | 375 | assert.dom().hasNoText('component unmounted'); 376 | 377 | await render( 378 | 388 | ); 389 | 390 | assert.dom().doesNotContainText('inactive'); 391 | assert.dom().containsText('active'); 392 | 393 | await click('button'); 394 | 395 | assert.dom().containsText('inactive'); 396 | }); 397 | 398 | test('can pass onTransition callback', async function (assert) { 399 | const Toggle = createMachine({ 400 | initial: 'inactive', 401 | states: { 402 | inactive: { on: { TOGGLE: 'active' } }, 403 | active: { on: { TOGGLE: 'inactive' } }, 404 | }, 405 | }); 406 | 407 | assert.expect(2); 408 | 409 | const doSomething = (snapshot: { value: string }) => { 410 | assert.step(snapshot.value); 411 | }; 412 | 413 | await render( 414 | 420 | ); 421 | 422 | assert.verifySteps(['active']); 423 | }); 424 | }); 425 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Release (2024-10-19) 4 | 5 | ember-statechart-component 7.1.0 (minor) 6 | 7 | #### :rocket: Enhancement 8 | * `ember-statechart-component` 9 | * [#502](https://github.com/NullVoxPopuli/ember-statechart-component/pull/502) Expose wrapper type ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 10 | 11 | #### :bug: Bug Fix 12 | * `ember-statechart-component` 13 | * [#503](https://github.com/NullVoxPopuli/ember-statechart-component/pull/503) Fix .matches alias method ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 14 | 15 | #### Committers: 1 16 | - [@NullVoxPopuli](https://github.com/NullVoxPopuli) 17 | 18 | ## Release (2024-10-17) 19 | 20 | ember-statechart-component 7.0.0 (major) 21 | 22 | Note that this version temporarily does not have _full_ support for nested actors. 23 | 24 | #### :boom: Breaking Change 25 | * `ember-statechart-component` 26 | * [#479](https://github.com/NullVoxPopuli/ember-statechart-component/pull/479) Implement support for XState 5 API and require type=module support ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 27 | * [#498](https://github.com/NullVoxPopuli/ember-statechart-component/pull/498) Drop support for XState < 5 ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 28 | * [#493](https://github.com/NullVoxPopuli/ember-statechart-component/pull/493) Drop support for the ember types from DT (@types/*) ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 29 | * [#483](https://github.com/NullVoxPopuli/ember-statechart-component/pull/483) set type=module ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 30 | * [#475](https://github.com/NullVoxPopuli/ember-statechart-component/pull/475) Upgrade to XState 5 ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 31 | * Other 32 | * [#481](https://github.com/NullVoxPopuli/ember-statechart-component/pull/481) Drop support for legacy conusmer builds ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 33 | * [#480](https://github.com/NullVoxPopuli/ember-statechart-component/pull/480) Drop support for ember-source < 5.1 ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 34 | * [#474](https://github.com/NullVoxPopuli/ember-statechart-component/pull/474) Drop support for TS < 5.2 ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 35 | 36 | #### :bug: Bug Fix 37 | * `ember-statechart-component` 38 | * [#435](https://github.com/NullVoxPopuli/ember-statechart-component/pull/435) Throw args update less often ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 39 | 40 | #### :memo: Documentation 41 | * [#459](https://github.com/NullVoxPopuli/ember-statechart-component/pull/459) docs: fix in readme setup section ([@MichalBryxi](https://github.com/MichalBryxi)) 42 | 43 | #### :house: Internal 44 | * Other 45 | * [#497](https://github.com/NullVoxPopuli/ember-statechart-component/pull/497) Update release-plan ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 46 | * [#495](https://github.com/NullVoxPopuli/ember-statechart-component/pull/495) Node 22 ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 47 | * [#482](https://github.com/NullVoxPopuli/ember-statechart-component/pull/482) Ignore-scripts=true when previewing release ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 48 | * [#468](https://github.com/NullVoxPopuli/ember-statechart-component/pull/468) Remove commitlint ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 49 | * [#457](https://github.com/NullVoxPopuli/ember-statechart-component/pull/457) Add CodeQL workflow for GitHub code scanning ([@lgtm-com[bot]](https://github.com/apps/lgtm-com)) 50 | * `ember-statechart-component` 51 | * [#496](https://github.com/NullVoxPopuli/ember-statechart-component/pull/496) Upgrade Glint ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 52 | * [#494](https://github.com/NullVoxPopuli/ember-statechart-component/pull/494) upgrade ts ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 53 | * [#492](https://github.com/NullVoxPopuli/ember-statechart-component/pull/492) Update pnpm ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 54 | * [#491](https://github.com/NullVoxPopuli/ember-statechart-component/pull/491) Upgrade lint dependencies ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 55 | * [#476](https://github.com/NullVoxPopuli/ember-statechart-component/pull/476) Upgrade lint changes ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 56 | * [#473](https://github.com/NullVoxPopuli/ember-statechart-component/pull/473) Upgrade tools, re-roll lockfile ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 57 | * [#469](https://github.com/NullVoxPopuli/ember-statechart-component/pull/469) Remove semantic release ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 58 | * [#466](https://github.com/NullVoxPopuli/ember-statechart-component/pull/466) Simplify ci ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 59 | * [#465](https://github.com/NullVoxPopuli/ember-statechart-component/pull/465) Setup release-plan ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) 60 | 61 | #### Committers: 3 62 | - Michal Bryxí ([@MichalBryxi](https://github.com/MichalBryxi)) 63 | - [@NullVoxPopuli](https://github.com/NullVoxPopuli) 64 | - [@lgtm-com[bot]](https://github.com/apps/lgtm-com) 65 | 66 | ## [6.1.2](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v6.1.1...v6.1.2) (2022-07-31) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * **args-update:** send ARGS_UPDATE less often ([dce62ad](https://github.com/NullVoxPopuli/ember-statechart-component/commit/dce62ad86fe3c9cce6b553a17c437379b32c245c)) 72 | * **package:** bump version ([9908645](https://github.com/NullVoxPopuli/ember-statechart-component/commit/9908645397588be51fc7961c06f0ea030547bd56)) 73 | 74 | ## [6.1.1](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v6.1.0...v6.1.1) (2022-07-03) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * **@config:** this arg is actually a `MachineOptions` type ([4fbb0f9](https://github.com/NullVoxPopuli/ember-statechart-component/commit/4fbb0f938b34193880f941c27e743b44795a5130)) 80 | 81 | # [6.1.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v6.0.1...v6.1.0) (2022-07-03) 82 | 83 | 84 | ### Features 85 | 86 | * automatic glint registration via importing /glint ([cfea170](https://github.com/NullVoxPopuli/ember-statechart-component/commit/cfea170bab7722078352242abb125b31dc823d45)) 87 | 88 | ## [6.0.1](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v6.0.0...v6.0.1) (2022-07-03) 89 | 90 | 91 | ### Bug Fixes 92 | 93 | * **types:** use correct version of xstate ([33409cf](https://github.com/NullVoxPopuli/ember-statechart-component/commit/33409cf1e44d34d6750d7363ff5ca77d012346f9)) 94 | 95 | # [6.0.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v5.0.5...v6.0.0) (2022-07-03) 96 | 97 | 98 | ### Features 99 | 100 | * bump minimum ember-source ([bd0a06c](https://github.com/NullVoxPopuli/ember-statechart-component/commit/bd0a06c1bd392a0627a5e156ea4e6dd383930f2c)) 101 | * glint support ([3509a18](https://github.com/NullVoxPopuli/ember-statechart-component/commit/3509a18a19a94027deb758b5f61afc7a602ea303)) 102 | * **glint:** provide type utilities for declaring machines as components ([1797ac8](https://github.com/NullVoxPopuli/ember-statechart-component/commit/1797ac8fca9a55a0d58f59a3416d937c461111a0)) 103 | 104 | 105 | ### BREAKING CHANGES 106 | 107 | * - minimum ember support is v4.0.0 108 | - automatic component manager registration is removed 109 | * minimum ember-source is 4 now 110 | 111 | ember-template-imports requires at least ember-source3.27 112 | and types are only compat with ember 4 113 | 114 | ## [5.0.5](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v5.0.4...v5.0.5) (2022-05-13) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * **deps:** update dependency xstate to v4.32.1 ([59442b8](https://github.com/NullVoxPopuli/ember-statechart-component/commit/59442b8a0cfc6d7f9aecc8542dae4d12a0633c94)) 120 | 121 | ## [5.0.4](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v5.0.3...v5.0.4) (2022-05-06) 122 | 123 | 124 | ### Bug Fixes 125 | 126 | * **deps:** update dependency xstate to v4.32.0 ([63fa0f2](https://github.com/NullVoxPopuli/ember-statechart-component/commit/63fa0f21378bfb7373b78b7125e8e346a4ce46cd)) 127 | 128 | ## [5.0.3](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v5.0.2...v5.0.3) (2022-04-11) 129 | 130 | 131 | ### Bug Fixes 132 | 133 | * **deps:** update dependency babel-plugin-ember-template-compilation to v1.0.2 ([da33c35](https://github.com/NullVoxPopuli/ember-statechart-component/commit/da33c359284be3bb6ef42951fd3c6ee726b35496)) 134 | 135 | ## [5.0.2](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v5.0.1...v5.0.2) (2022-04-08) 136 | 137 | 138 | ### Bug Fixes 139 | 140 | * **deps:** update dependency xstate to v4.31.0 ([5ff8435](https://github.com/NullVoxPopuli/ember-statechart-component/commit/5ff84353e419d8c92c17146ec0bcd1e946e01fc1)) 141 | 142 | ## [5.0.1](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v5.0.0...v5.0.1) (2022-04-08) 143 | 144 | 145 | ### Bug Fixes 146 | 147 | * **deps:** update embroider monorepo to v1.6.0 ([e73c84d](https://github.com/NullVoxPopuli/ember-statechart-component/commit/e73c84d822ec1b8a146b8cb24ec3900187a3df06)) 148 | 149 | # [5.0.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v4.1.2...v5.0.0) (2022-03-22) 150 | 151 | 152 | ### Bug Fixes 153 | 154 | * **types:** minor update in xstate cause breaking type change ([e786af7](https://github.com/NullVoxPopuli/ember-statechart-component/commit/e786af7cf16683cf12eeaacf1b4efbd206418b21)) 155 | 156 | 157 | ### BREAKING CHANGES 158 | 159 | * **types:** support latest xstate, ^4.30.0 160 | 161 | ## [4.1.2](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v4.1.1...v4.1.2) (2022-01-27) 162 | 163 | 164 | ### Bug Fixes 165 | 166 | * **deps:** update dependency xstate to v4.29.0 ([7bb7773](https://github.com/NullVoxPopuli/ember-statechart-component/commit/7bb7773d90abdea339b7d616d8cd66d848ee31ab)) 167 | 168 | ## [4.1.1](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v4.1.0...v4.1.1) (2022-01-21) 169 | 170 | 171 | ### Bug Fixes 172 | 173 | * **deps:** update dependency xstate to v4.28.1 ([9bc493f](https://github.com/NullVoxPopuli/ember-statechart-component/commit/9bc493fd903f457e42c1cc5989a361b66a46081d)) 174 | 175 | # [4.1.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v4.0.1...v4.1.0) (2022-01-20) 176 | 177 | 178 | ### Features 179 | 180 | * exposes onTransition helper ([0c54bab](https://github.com/NullVoxPopuli/ember-statechart-component/commit/0c54bab0e72b72cd733ed2aeb609e433010814b8)) 181 | 182 | ## [4.0.1](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v4.0.0...v4.0.1) (2022-01-20) 183 | 184 | 185 | ### Bug Fixes 186 | 187 | * **deps:** update dependency xstate to v4.28.0 ([5f61f8d](https://github.com/NullVoxPopuli/ember-statechart-component/commit/5f61f8d031e9b002f71494ed7325cc085d45d920)) 188 | 189 | # [4.0.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v3.2.3...v4.0.0) (2022-01-09) 190 | 191 | 192 | ### Features 193 | 194 | * merge context by default ([74d921c](https://github.com/NullVoxPopuli/ember-statechart-component/commit/74d921c2845febd09dbf5832aa646cedb9a3f94c)) 195 | * merge passing context by default ([643a8c6](https://github.com/NullVoxPopuli/ember-statechart-component/commit/643a8c677359de0f8a94d50547f0417e7ee1e53a)), closes [#204](https://github.com/NullVoxPopuli/ember-statechart-component/issues/204) 196 | 197 | 198 | ### BREAKING CHANGES 199 | 200 | * context is now merged instead of replaced 201 | 202 | ## [3.2.3](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v3.2.2...v3.2.3) (2022-01-08) 203 | 204 | 205 | ### Bug Fixes 206 | 207 | * **revert:** spawn fix accidentally introduced infinite loop ([78e2582](https://github.com/NullVoxPopuli/ember-statechart-component/commit/78e2582ac0b10d67ce95abcd49c44616526af069)) 208 | 209 | ## [3.2.2](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v3.2.1...v3.2.2) (2022-01-08) 210 | 211 | 212 | ### Bug Fixes 213 | 214 | * **spawn:** for spawn machines stored on nested context data, support reactivity ([3a1ca73](https://github.com/NullVoxPopuli/ember-statechart-component/commit/3a1ca739fa29e91cdd6d4e139eac428a7249f84e)) 215 | 216 | ## [3.2.1](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v3.2.0...v3.2.1) (2021-12-30) 217 | 218 | 219 | ### Bug Fixes 220 | 221 | * **deps:** update dependency xstate to v4.27.0 ([d3cfe32](https://github.com/NullVoxPopuli/ember-statechart-component/commit/d3cfe3230009afe0cc91de44fcabcfe989e5ba47)) 222 | 223 | # [3.2.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v3.1.0...v3.2.0) (2021-12-23) 224 | 225 | 226 | ### Features 227 | 228 | * **manager:** official support spawn and invoke ([a34d193](https://github.com/NullVoxPopuli/ember-statechart-component/commit/a34d193baf20ffb0963770219e8e4792d9c6c604)) 229 | 230 | # [3.1.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v3.0.1...v3.1.0) (2021-12-12) 231 | 232 | 233 | ### Features 234 | 235 | * **internal:** convert to the Addon@v2 format ([54fca76](https://github.com/NullVoxPopuli/ember-statechart-component/commit/54fca76627453423782096cf5d685abe1dfde0aa)) 236 | 237 | ## [3.0.1](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v3.0.0...v3.0.1) (2021-12-06) 238 | 239 | 240 | ### Bug Fixes 241 | 242 | * **deps:** update dependency ember-cli-htmlbars to v6.0.1 ([c872839](https://github.com/NullVoxPopuli/ember-statechart-component/commit/c872839cc2e9add8c493369cb6f1b55b001964e2)) 243 | 244 | # [3.0.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.3.7...v3.0.0) (2021-12-05) 245 | 246 | 247 | ### Bug Fixes 248 | 249 | * **deps:** update dependency tracked-maps-and-sets to v3 ([40618e9](https://github.com/NullVoxPopuli/ember-statechart-component/commit/40618e92be004c2a595ceaacac3c51ab3c10151c)) 250 | 251 | 252 | ### chore 253 | 254 | * drop support for node 12 ([b1b3c4c](https://github.com/NullVoxPopuli/ember-statechart-component/commit/b1b3c4c64e0b745c43c2dfe9f745757d71c7f2f3)) 255 | * **ci:** update test matrix ([91de3bb](https://github.com/NullVoxPopuli/ember-statechart-component/commit/91de3bb2e80c8338998d4b6a7f7ddef2e650b68c)) 256 | * **deps:** pin @babel/helper-create-class-features-plugin ([1a8111b](https://github.com/NullVoxPopuli/ember-statechart-component/commit/1a8111b364cdd6d162982784c6c754187735f9f3)), closes [/github.com/babel/ember-cli-babel/issues/419#issuecomment-985735309](https://github.com//github.com/babel/ember-cli-babel/issues/419/issues/issuecomment-985735309) 257 | 258 | 259 | ### BREAKING CHANGES 260 | 261 | * node 12 is no longer supported 262 | * **ci:** drop support for ember 3.26 263 | * **deps:** this package now requires ember-auto-import@v2 264 | 265 | ## [2.3.7](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.3.6...v2.3.7) (2021-08-13) 266 | 267 | 268 | ### Bug Fixes 269 | 270 | * **deps:** update dependency ember-auto-import to ^1.12.0 ([84b04e8](https://github.com/NullVoxPopuli/ember-statechart-component/commit/84b04e8885c4a7ccc270c34cbcc184e2596ed2b2)) 271 | 272 | ## [2.3.6](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.3.5...v2.3.6) (2021-06-17) 273 | 274 | 275 | ### Bug Fixes 276 | 277 | * **deps:** update dependency ember-cli-typescript to ^4.2.1 ([279635e](https://github.com/NullVoxPopuli/ember-statechart-component/commit/279635efb4f736fce3d18903231e61d812402c39)) 278 | 279 | ## [2.3.5](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.3.4...v2.3.5) (2021-06-15) 280 | 281 | 282 | ### Bug Fixes 283 | 284 | * **deps:** update dependency ember-cli-typescript to ^4.2.0 ([618aca6](https://github.com/NullVoxPopuli/ember-statechart-component/commit/618aca6cf1dc470992ccd6d5d0d4f3094788e83d)) 285 | 286 | ## [2.3.4](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.3.3...v2.3.4) (2021-05-18) 287 | 288 | 289 | ### Bug Fixes 290 | 291 | * **deps:** update dependency ember-cli-babel to ^7.26.6 ([9edf718](https://github.com/NullVoxPopuli/ember-statechart-component/commit/9edf718724e6b914e7eaebfb007b2cf197949b51)) 292 | 293 | ## [2.3.3](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.3.2...v2.3.3) (2021-05-10) 294 | 295 | 296 | ### Bug Fixes 297 | 298 | * **readme:** add browser requirements ([252f1a0](https://github.com/NullVoxPopuli/ember-statechart-component/commit/252f1a0503700675c8f217bb4069fd60c6ba3009)), closes [PR#33](https://github.com/PR/issues/33) 299 | 300 | ## [2.3.2](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.3.1...v2.3.2) (2021-05-06) 301 | 302 | 303 | ### Bug Fixes 304 | 305 | * **deps:** update dependency ember-cli-babel to ^7.26.5 ([635417f](https://github.com/NullVoxPopuli/ember-statechart-component/commit/635417f81f41fcd38a181f33a164d815fa4ba3fc)) 306 | 307 | ## [2.3.1](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.3.0...v2.3.1) (2021-04-30) 308 | 309 | 310 | ### Bug Fixes 311 | 312 | * **runloop:** prevent infinite invalidation bugs ([fae96bd](https://github.com/NullVoxPopuli/ember-statechart-component/commit/fae96bd64b35044c1b2fbb9f09e9273e8cdf5ce1)) 313 | 314 | # [2.3.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.2.3...v2.3.0) (2021-04-29) 315 | 316 | 317 | ### Bug Fixes 318 | 319 | * **deps:** move ember-auto-import to dependencies ([fac6ac0](https://github.com/NullVoxPopuli/ember-statechart-component/commit/fac6ac0550cc26a4024d655815308ed0d425d3d3)) 320 | 321 | 322 | ### Features 323 | 324 | * support embroider ([6044337](https://github.com/NullVoxPopuli/ember-statechart-component/commit/6044337f79f0d6f06d89c126c1114f6fd60e9c65)) 325 | 326 | ## [2.2.3](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.2.2...v2.2.3) (2021-04-28) 327 | 328 | 329 | ### Bug Fixes 330 | 331 | * **deps:** update dependency ember-cli-babel to ^7.26.4 ([c30df0a](https://github.com/NullVoxPopuli/ember-statechart-component/commit/c30df0a7c59b1fb94d263fd63bcc48909b121b61)) 332 | 333 | ## [2.2.2](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.2.1...v2.2.2) (2021-04-24) 334 | 335 | 336 | ### Bug Fixes 337 | 338 | * **deps:** update dependency tracked-built-ins to ^1.1.1 ([9057cd7](https://github.com/NullVoxPopuli/ember-statechart-component/commit/9057cd76b5d416d5a124d7b436c05b1852f666fa)) 339 | 340 | ## [2.2.1](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.2.0...v2.2.1) (2021-04-24) 341 | 342 | 343 | ### Bug Fixes 344 | 345 | * **internal:** fill out missing package.json fields ([fed4577](https://github.com/NullVoxPopuli/ember-statechart-component/commit/fed4577d59392ff93e2f15686092bee4dca24003)) 346 | 347 | # [2.2.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.1.0...v2.2.0) (2021-04-24) 348 | 349 | 350 | ### Features 351 | 352 | * support accessing services ([446ac64](https://github.com/NullVoxPopuli/ember-statechart-component/commit/446ac648f8a2d209d16b29603e8c73ee30e3f163)) 353 | 354 | # [2.1.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v2.0.0...v2.1.0) (2021-04-23) 355 | 356 | 357 | ### Features 358 | 359 | * new default template ([e3be573](https://github.com/NullVoxPopuli/ember-statechart-component/commit/e3be5732ba404983e220526248edf0043cc818d7)) 360 | 361 | # [2.0.0](https://github.com/NullVoxPopuli/ember-statechart-component/compare/v1.0.0...v2.0.0) (2021-04-23) 362 | 363 | 364 | ### Features 365 | 366 | * initial release ([64a1aec](https://github.com/NullVoxPopuli/ember-statechart-component/commit/64a1aece7cef6bde06c9f5739ea427d88b6e7c2c)) 367 | 368 | 369 | ### BREAKING CHANGES 370 | 371 | * first release to NPM 372 | 373 | # 1.0.0 (2021-04-23) 374 | 375 | 376 | ### Bug Fixes 377 | 378 | * **ci:** invoke semantic-release directly ([2d12cbe](https://github.com/NullVoxPopuli/ember-statechart-component/commit/2d12cbe23950c0b167a74b78980a69d517e48919)) 379 | 380 | 381 | ### Features 382 | 383 | * basic reactivity implemented ([1bc4e28](https://github.com/NullVoxPopuli/ember-statechart-component/commit/1bc4e28500b1a3ca10a92f34ce64170e55aa1365)) 384 | 385 | 386 | ### BREAKING CHANGES 387 | 388 | * first release 389 | --------------------------------------------------------------------------------