├── vendor └── .gitkeep ├── packages ├── site │ ├── app │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ ├── .gitkeep │ │ │ └── application.ts │ │ ├── components │ │ │ ├── .gitkeep │ │ │ ├── ui-loading.hbs │ │ │ ├── docs-header.js │ │ │ ├── ui-button.hbs │ │ │ ├── api-toc.js │ │ │ ├── mobile-docs-toc.ts │ │ │ ├── docs-viewer.js │ │ │ ├── typed-button.hbs │ │ │ ├── quickstart-button.hbs │ │ │ ├── quickstart-button-refined.hbs │ │ │ ├── page-headings.ts │ │ │ ├── docs │ │ │ │ ├── quickstart-guide.js │ │ │ │ ├── typescript-usage.js │ │ │ │ ├── quickstart-final.js │ │ │ │ ├── quickstart-guide.hbs │ │ │ │ ├── typescript-usage.hbs │ │ │ │ └── quickstart-final.hbs │ │ │ ├── counter.hbs │ │ │ ├── counter-restart.hbs │ │ │ ├── mobile-docs-toc.hbs │ │ │ ├── counter-restart.js │ │ │ ├── docs-toc.hbs │ │ │ ├── counter.js │ │ │ ├── demo.hbs │ │ │ ├── ui-intro.md │ │ │ ├── api-toc.hbs │ │ │ ├── quickstart-button.js │ │ │ ├── page-headings.hbs │ │ │ ├── docs-header.hbs │ │ │ ├── quickstart-button-refined.js │ │ │ └── typed-button.ts │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ ├── docs │ │ │ │ └── statecharts.js │ │ │ └── docs.ts │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── router.js │ │ ├── snippets │ │ │ └── quickstart │ │ │ │ ├── onExit.js │ │ │ │ ├── onEntry.js │ │ │ │ └── transition.js │ │ ├── app.js │ │ ├── config │ │ │ └── environment.d.ts │ │ ├── index.html │ │ ├── machines │ │ │ ├── quickstart-button.js │ │ │ ├── counter-machine.js │ │ │ ├── quickstart-button-refined.js │ │ │ └── typed-button.ts │ │ ├── utils │ │ │ └── scroll-to.ts │ │ ├── styles │ │ │ └── app.css │ │ ├── templates │ │ │ └── docs.hbs │ │ └── modifiers │ │ │ └── intersect-headings.js │ ├── tests │ │ ├── unit │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── integration │ │ │ ├── .gitkeep │ │ │ └── components │ │ │ │ └── x-button-test.js │ │ ├── test-helper.js │ │ └── index.html │ ├── public │ │ ├── CNAME │ │ ├── robots.txt │ │ ├── tutorial.png │ │ ├── xstate-viz.png │ │ ├── harel-paper-1.png │ │ ├── xstate-guides.png │ │ ├── statecharts-blog.png │ │ ├── harel-statechart-paper.png │ │ ├── quote.svg │ │ ├── effective-ember.svg │ │ ├── youtube.svg │ │ ├── discord.svg │ │ ├── xstate.svg │ │ ├── ember-statecharts-black.svg │ │ ├── ember-statecharts-white.svg │ │ └── statecharts.svg │ ├── .template-lintrc.js │ ├── .eslintrc.js │ ├── lib │ │ ├── deploy-versioned-doc │ │ │ ├── package.json │ │ │ └── index.js │ │ └── versioned-doc-info.js │ ├── config │ │ ├── optional-features.json │ │ ├── deploy.js │ │ ├── targets.js │ │ ├── ember-cli-update.json │ │ └── environment.js │ ├── tsconfig.json │ ├── tailwind.config.js │ ├── .prettierignore │ ├── .eslintignore │ ├── .gitignore │ ├── testem.js │ ├── .docfy-config.js │ ├── ember-cli-build.js │ └── package.json ├── test-app │ ├── vendor │ │ └── .gitkeep │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ ├── components │ │ │ ├── .gitkeep │ │ │ ├── use-machine.hbs │ │ │ ├── computed.hbs │ │ │ ├── computed.js │ │ │ └── use-machine.js │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── templates │ │ │ └── application.hbs │ │ ├── router.js │ │ ├── machines │ │ │ └── toggle-machine.js │ │ ├── app.js │ │ └── index.html │ ├── tests │ │ ├── unit │ │ │ └── .gitkeep │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── integration │ │ │ └── .gitkeep │ │ ├── test-helper.js │ │ ├── index.html │ │ └── acceptance │ │ │ └── smoke-test.js │ ├── .watchmanconfig │ ├── public │ │ └── robots.txt │ ├── .prettierrc.js │ ├── .template-lintrc.js │ ├── config │ │ ├── optional-features.json │ │ ├── ember-cli-update.json │ │ ├── targets.js │ │ ├── environment.js │ │ └── ember-try.js │ ├── .ember-cli │ ├── .prettierignore │ ├── .eslintignore │ ├── .editorconfig │ ├── .travis.yml │ ├── .gitignore │ ├── testem.js │ ├── ember-cli-build.js │ ├── .eslintrc.js │ ├── README.md │ └── package.json └── ember-statecharts │ ├── addon │ ├── .gitkeep │ ├── utils │ │ └── statechart.js │ ├── computed.js │ ├── -private │ │ ├── use-resource.ts │ │ ├── resource.ts │ │ └── usables.ts │ └── index.ts │ ├── app │ ├── .gitkeep │ └── utils │ │ └── statechart.js │ ├── tests │ ├── unit │ │ ├── .gitkeep │ │ ├── statechart-test.js │ │ └── computed-test.js │ ├── helpers │ │ └── .gitkeep │ ├── integration │ │ └── .gitkeep │ ├── dummy │ │ ├── app │ │ │ ├── models │ │ │ │ └── .gitkeep │ │ │ ├── routes │ │ │ │ └── .gitkeep │ │ │ ├── styles │ │ │ │ └── app.css │ │ │ ├── components │ │ │ │ └── .gitkeep │ │ │ ├── controllers │ │ │ │ └── .gitkeep │ │ │ ├── helpers │ │ │ │ └── .gitkeep │ │ │ ├── templates │ │ │ │ └── application.hbs │ │ │ ├── router.js │ │ │ ├── app.js │ │ │ ├── config │ │ │ │ └── environment.d.ts │ │ │ └── index.html │ │ ├── public │ │ │ └── .gitkeep │ │ └── config │ │ │ ├── optional-features.json │ │ │ ├── ember-cli-update.json │ │ │ ├── targets.js │ │ │ └── environment.js │ ├── test-helper.js │ └── index.html │ ├── index.js │ ├── .template-lintrc.js │ ├── .eslintrc.js │ ├── types │ ├── @glimmer │ │ └── tracking │ │ │ └── primitives │ │ │ └── cache │ │ │ └── index.d.ts │ └── @ember │ │ └── helper │ │ └── index.d.ts │ ├── config │ └── environment.js │ ├── tsconfig.json │ ├── .prettierignore │ ├── .eslintignore │ ├── -private │ ├── resource.d.ts │ ├── use-resource.d.ts │ └── usables.d.ts │ ├── .gitignore │ ├── .npmignore │ ├── testem.js │ ├── ember-cli-build.js │ ├── README.md │ ├── index.d.ts │ └── package.json ├── .watchmanconfig ├── .prettierrc.js ├── jsconfig.json ├── types ├── dummy │ └── index.d.ts ├── global.d.ts └── ember-metrics │ └── index.d.ts ├── lerna.json ├── .ember-cli ├── .eslintignore ├── .prettierignore ├── .editorconfig ├── .vscode └── launch.json ├── package.json ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── tsconfig.settings.json ├── .github └── workflows │ ├── docs.yml │ └── ci.yml ├── .eslintrc.base.js └── README.md /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/site/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/site/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/site/tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/test-app/vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/site/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/site/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/site/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/site/tests/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/test-app/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/test-app/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/test-app/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/test-app/app/styles/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/test-app/tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ember-statecharts/addon/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ember-statecharts/app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/site/tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/test-app/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/test-app/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/test-app/tests/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/test-app/tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/integration/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/site/public/CNAME: -------------------------------------------------------------------------------- 1 | ember-statecharts.com 2 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/public/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /packages/test-app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/site/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/test-app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /types/dummy/index.d.ts: -------------------------------------------------------------------------------- 1 | import 'ember-concurrency-async'; 2 | import 'ember-concurrency-ts/async'; 3 | -------------------------------------------------------------------------------- /packages/site/.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'octane', 5 | }; 6 | -------------------------------------------------------------------------------- /packages/test-app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | singleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/ember-statecharts/app/utils/statechart.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-statecharts/utils/statechart'; 2 | -------------------------------------------------------------------------------- /packages/site/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: '../../.eslintrc.base.js', 5 | }; 6 | -------------------------------------------------------------------------------- /packages/test-app/.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /packages/ember-statecharts/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/ember-statecharts/.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended', 5 | }; 6 | -------------------------------------------------------------------------------- /packages/site/public/tutorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/ember-statecharts/master/packages/site/public/tutorial.png -------------------------------------------------------------------------------- /packages/ember-statecharts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: '../../.eslintrc.base.js', 5 | }; 6 | -------------------------------------------------------------------------------- /packages/site/app/components/ui-loading.hbs: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /packages/site/public/xstate-viz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/ember-statecharts/master/packages/site/public/xstate-viz.png -------------------------------------------------------------------------------- /packages/ember-statecharts/types/@glimmer/tracking/primitives/cache/index.d.ts: -------------------------------------------------------------------------------- 1 | export { createCache, getValue } from '@glimmer/validator'; 2 | -------------------------------------------------------------------------------- /packages/site/public/harel-paper-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/ember-statecharts/master/packages/site/public/harel-paper-1.png -------------------------------------------------------------------------------- /packages/site/public/xstate-guides.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/ember-statecharts/master/packages/site/public/xstate-guides.png -------------------------------------------------------------------------------- /packages/site/public/statecharts-blog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/ember-statecharts/master/packages/site/public/statecharts-blog.png -------------------------------------------------------------------------------- /packages/ember-statecharts/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (/* environment, appConfig */) { 4 | return {}; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/site/public/harel-statechart-paper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pangratz/ember-statecharts/master/packages/site/public/harel-statechart-paper.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "useWorkspaces": true, 4 | "version": "independent", 5 | "packages": [ 6 | "packages/*" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/site/app/components/docs-header.js: -------------------------------------------------------------------------------- 1 | import DocsHeader from 'ember-cli-addon-docs/components/docs-header/component'; 2 | 3 | export default DocsHeader; 4 | -------------------------------------------------------------------------------- /packages/site/lib/deploy-versioned-doc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deploy-versioned-doc", 3 | "keywords": [ 4 | "ember-addon", 5 | "ember-cli-deploy-plugin" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/ember-statecharts/types/@ember/helper/index.d.ts: -------------------------------------------------------------------------------- 1 | export { 2 | setHelperManager, 3 | helperCapabilities as capabilities, 4 | } from '@glimmer/manager'; 5 | export { invokeHelper } from '@glimmer/runtime'; 6 | -------------------------------------------------------------------------------- /packages/site/app/components/ui-button.hbs: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /packages/site/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 | } 7 | -------------------------------------------------------------------------------- /packages/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 | } 7 | -------------------------------------------------------------------------------- /packages/site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "composite": true 5 | }, 6 | "include": [ 7 | "app/**/*", 8 | "types/**/*" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | // Types for compiled templates 2 | declare module 'ember-statecharts/templates/*' { 3 | import { TemplateFactory } from 'htmlbars-inline-precompile'; 4 | const tmpl: TemplateFactory; 5 | export default tmpl; 6 | } 7 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/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 | } 7 | -------------------------------------------------------------------------------- /packages/test-app/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{page-title 'TestApp'}} 2 | 3 |

4 | Ember statecharts smoke tests 5 |

6 | 7 | {{outlet}} 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/site/app/components/api-toc.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import config from 'site/config/environment'; 3 | 4 | export default class ApiTocComponent extends Component { 5 | get rootUrl() { 6 | return config.rootURL; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/test-app/app/router.js: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'dummy/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 | -------------------------------------------------------------------------------- /packages/site/config/deploy.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = function (/* deployTarget */) { 5 | let ENV = { 6 | build: {}, 7 | git: { 8 | repo: 'git@github.com:pangratz/ember-statecharts.git', 9 | commitMessage: 'chore: deploy %@', 10 | }, 11 | }; 12 | return ENV; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/test-app/.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.settings.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "baseUrl": "." 6 | }, 7 | "include": [ 8 | "app/**/*", 9 | "addon/**/*", 10 | "tests/**/*", 11 | "types/**/*", 12 | "test-support/**/*", 13 | "addon-test-support/**/*" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/site/app/components/mobile-docs-toc.ts: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { tracked } from '@glimmer/tracking'; 3 | import { action } from '@ember/object'; 4 | 5 | export default class MobileDocsToc extends Component { 6 | @tracked showToc = false; 7 | 8 | @action toggleToc(): void { 9 | this.showToc = !this.showToc; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/test-app/.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /packages/ember-statecharts/.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | -------------------------------------------------------------------------------- /packages/site/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from 'site/config/environment'; 3 | import { addDocfyRoutes } from '@docfy/ember'; 4 | 5 | export default class Router extends EmberRouter { 6 | location = config.locationType; 7 | rootURL = config.rootURL; 8 | } 9 | 10 | Router.map(function () { 11 | addDocfyRoutes(this); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/test-app/.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .*/ 17 | .eslintcache 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /package.json.ember-try 23 | -------------------------------------------------------------------------------- /types/ember-metrics/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'ember-metrics/services/metrics' { 2 | import Service from '@ember/service'; 3 | 4 | export default class Metrics extends Service { 5 | trackEvent(params: Record): void; 6 | 7 | trackPage(params: Record): void; 8 | trackPage(adapterName: string, params: Record): void; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/ember-statecharts/.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /**/dist/ 7 | /**/tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .*/ 17 | .eslintcache 18 | 19 | # ember-try 20 | /.node_modules.ember-try/ 21 | /bower.json.ember-try 22 | /package.json.ember-try 23 | -------------------------------------------------------------------------------- /packages/site/tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'site/app'; 2 | import config from 'site/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /packages/test-app/app/components/use-machine.hbs: -------------------------------------------------------------------------------- 1 |
2 |

3 | use-machine 4 |

5 | 8 |
9 | {{#if this.isOn}} 10 | On 11 | {{/if}} 12 | {{#if this.isOff}} 13 | Off 14 | {{/if}} 15 |
16 |
17 | -------------------------------------------------------------------------------- /packages/test-app/app/components/computed.hbs: -------------------------------------------------------------------------------- 1 |
2 |

3 | @statechart-computed 4 |

5 | 8 |
9 | {{#if this.isOn}} 10 | On 11 | {{/if}} 12 | {{#if this.isOff}} 13 | Off 14 | {{/if}} 15 |
16 |
17 | -------------------------------------------------------------------------------- /packages/test-app/app/machines/toggle-machine.js: -------------------------------------------------------------------------------- 1 | import { createMachine } from 'xstate'; 2 | 3 | export const toggleConfig = { 4 | initial: 'off', 5 | states: { 6 | off: { 7 | on: { 8 | TOGGLE: 'on', 9 | }, 10 | }, 11 | on: { 12 | on: { 13 | TOGGLE: 'off', 14 | }, 15 | }, 16 | }, 17 | }; 18 | 19 | export default createMachine(toggleConfig); 20 | -------------------------------------------------------------------------------- /packages/site/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: { 6 | colors: { 7 | ember: '#e04e39', 8 | }, 9 | }, 10 | }, 11 | variants: { 12 | extend: {}, 13 | }, 14 | plugins: [ 15 | require('@tailwindcss/typography'), 16 | require('@tailwindcss/aspect-ratio'), 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /packages/test-app/tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'test-app/app'; 2 | import config from 'test-app/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from 'dummy/app'; 2 | import config from 'dummy/config/environment'; 3 | import * as QUnit from 'qunit'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setup } from 'qunit-dom'; 6 | import { start } from 'ember-qunit'; 7 | 8 | setApplication(Application.create(config.APP)); 9 | 10 | setup(QUnit.assert); 11 | 12 | start(); 13 | -------------------------------------------------------------------------------- /packages/site/.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | 23 | # api-docs 24 | /public/api-docs/ 25 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /**/dist/ 7 | /**/tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | 23 | # api-docs 24 | /packages/site/public/api-docs/ 25 | -------------------------------------------------------------------------------- /packages/site/.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /**/dist/ 7 | /**/tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | 23 | # api-docs 24 | /public/api-docs/ 25 | -------------------------------------------------------------------------------- /packages/site/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 | const isCI = Boolean(process.env.CI); 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers, 18 | }; 19 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /**/dist/ 7 | /**/tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | .eslintcache 17 | 18 | # ember-try 19 | /.node_modules.ember-try/ 20 | /bower.json.ember-try 21 | /package.json.ember-try 22 | 23 | # api-docs 24 | /packages/site/public/api-docs/ 25 | -------------------------------------------------------------------------------- /packages/site/app/components/docs-viewer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import Component from '@ember/component'; 3 | import layout from 'ember-cli-addon-docs/components/docs-viewer/template'; 4 | import { inject as service } from '@ember/service'; 5 | 6 | export default Component.extend({ 7 | layout, 8 | docsRoutes: service(), 9 | router: service(), 10 | 11 | classNames: 'docs-viewer docs-flex docs-flex1', 12 | }); 13 | -------------------------------------------------------------------------------- /packages/site/app/snippets/quickstart/onExit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default // BEGIN-SNIPPET quickstart-on-exit 3 | ({ 4 | initial: 'idle', 5 | states: { 6 | idle: { 7 | on: { 8 | SUBMIT: 'busy', 9 | }, 10 | exit: ['handleSubmit'], 11 | }, 12 | busy: {}, 13 | }, 14 | }, 15 | { 16 | actions: { 17 | handleSubmit(/*context, event*/) {}, 18 | }, 19 | }); 20 | // END-SNIPPET 21 | -------------------------------------------------------------------------------- /packages/site/app/snippets/quickstart/onEntry.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default // BEGIN-SNIPPET quickstart-on-entry 3 | ({ 4 | initial: 'idle', 5 | states: { 6 | idle: { 7 | on: { 8 | SUBMIT: 'busy', 9 | }, 10 | }, 11 | busy: { 12 | entry: ['handleSubmit'], 13 | }, 14 | }, 15 | }, 16 | { 17 | actions: { 18 | handleSubmit(/*context, event*/) {}, 19 | }, 20 | }); 21 | // END-SNIPPET 22 | -------------------------------------------------------------------------------- /packages/site/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'site/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /packages/test-app/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'test-app/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /packages/site/app/config/environment.d.ts: -------------------------------------------------------------------------------- 1 | export default config; 2 | 3 | /** 4 | * Type declarations for 5 | * import config from './config/environment' 6 | * 7 | * For now these need to be managed by the developer 8 | * since different ember addons can materialize new entries. 9 | */ 10 | declare const config: { 11 | environment: any; 12 | modulePrefix: string; 13 | podModulePrefix: string; 14 | locationType: string; 15 | rootURL: string; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'dummy/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /packages/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 | -------------------------------------------------------------------------------- /packages/site/app/snippets/quickstart/transition.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default // BEGIN-SNIPPET quickstart-transition 3 | ({ 4 | initial: 'idle', 5 | states: { 6 | idle: { 7 | on: { 8 | SUBMIT: { 9 | target: 'busy', 10 | actions: ['handleSubmit'], 11 | }, 12 | }, 13 | }, 14 | busy: {}, 15 | }, 16 | }, 17 | { 18 | actions: { 19 | handleSubmit(/*context, event*/) {}, 20 | }, 21 | }); 22 | // END-SNIPPET 23 | -------------------------------------------------------------------------------- /packages/test-app/.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "12" 5 | 6 | dist: xenial 7 | 8 | addons: 9 | chrome: stable 10 | 11 | cache: 12 | yarn: true 13 | 14 | env: 15 | global: 16 | # See https://git.io/vdao3 for details. 17 | - JOBS=1 18 | 19 | branches: 20 | only: 21 | - master 22 | 23 | before_install: 24 | - curl -o- -L https://yarnpkg.com/install.sh | bash 25 | - export PATH=$HOME/.yarn/bin:$PATH 26 | 27 | script: 28 | - yarn test 29 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/app/config/environment.d.ts: -------------------------------------------------------------------------------- 1 | export default config; 2 | 3 | /** 4 | * Type declarations for 5 | * import config from './config/environment' 6 | * 7 | * For now these need to be managed by the developer 8 | * since different ember addons can materialize new entries. 9 | */ 10 | declare const config: { 11 | environment: any; 12 | modulePrefix: string; 13 | podModulePrefix: string; 14 | locationType: string; 15 | rootURL: string; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/ember-statecharts/-private/resource.d.ts: -------------------------------------------------------------------------------- 1 | declare type Owner = Record; 2 | export interface TemplateArgs { 3 | positional: readonly unknown[]; 4 | named: Record; 5 | } 6 | export declare abstract class Resource< 7 | Args extends TemplateArgs = TemplateArgs 8 | > { 9 | protected readonly args: Args; 10 | constructor(owner: Owner, args: Args); 11 | setup(): void; 12 | update?(args: Args): void; 13 | teardown?(): void; 14 | } 15 | export {}; 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug ember serve", 11 | "program": "${workspaceFolder}/node_modules/ember-cli/bin/ember", 12 | "args": ["serve"] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /packages/test-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /connect.lock 17 | /coverage/ 18 | /libpeerconnection.log 19 | /npm-debug.log* 20 | /testem.log 21 | /yarn-error.log 22 | 23 | # ember-try 24 | /.node_modules.ember-try/ 25 | /bower.json.ember-try 26 | /package.json.ember-try 27 | -------------------------------------------------------------------------------- /packages/test-app/app/components/computed.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { statechart, matchesState } from 'ember-statecharts/computed'; 3 | import { toggleConfig } from '../machines/toggle-machine'; 4 | import { action } from '@ember/object'; 5 | 6 | export default class ComputedTest extends Component { 7 | @statechart(toggleConfig) statechart; 8 | 9 | @matchesState('on') isOn; 10 | @matchesState('off') isOff; 11 | 12 | @action toggle() { 13 | this.statechart.send('TOGGLE'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/site/app/controllers/docs/statecharts.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action } from '@ember/object'; 3 | import { tracked } from '@glimmer/tracking'; 4 | 5 | export default class StatechartsController extends Controller { 6 | @tracked 7 | counterCount = 0; 8 | @tracked 9 | count = 0; 10 | 11 | @action 12 | updateCount({ target: { value } }) { 13 | this.count = value; 14 | } 15 | 16 | @action 17 | syncCounterCount() { 18 | this.counterCount = +this.count; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-statecharts-root", 3 | "version": "0.13.2", 4 | "private": true, 5 | "description": "Statecharts for Ember.js applications", 6 | "homepage": "https://ember-statecharts.com/", 7 | "repository": "https://github.com/LevelbossMike/ember-statecharts", 8 | "license": "MIT", 9 | "author": "", 10 | "workspaces": [ 11 | "packages/*" 12 | ], 13 | "devDependencies": { 14 | "lerna": "^4.0.0" 15 | }, 16 | "volta": { 17 | "node": "14.18.1", 18 | "yarn": "1.22.17" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/site/app/components/typed-button.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET typed-button }} 2 | 19 | {{! END-SNIPPET }} 20 | -------------------------------------------------------------------------------- /packages/site/app/components/quickstart-button.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET quickstart-button }} 2 | 19 | {{! END-SNIPPET }} 20 | -------------------------------------------------------------------------------- /packages/site/public/quote.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /packages/site/app/components/quickstart-button-refined.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET quickstart-button-final }} 2 | 19 | {{! END-SNIPPET }} 20 | -------------------------------------------------------------------------------- /packages/test-app/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.28.4", 7 | "blueprints": [ 8 | { 9 | "name": "app", 10 | "outputRepo": "https://github.com/ember-cli/ember-new-output", 11 | "codemodsSource": "ember-app-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [ 14 | "--no-welcome", 15 | "--yarn" 16 | ] 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /packages/site/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.24.0", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [ 14 | "--yarn", 15 | "--no-welcome" 16 | ] 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /**/dist/ 5 | /**/tmp/ 6 | 7 | # dependencies 8 | /**/bower_components/ 9 | /**/node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /*.tsbuildinfo 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 | 29 | .DS_Store 30 | 31 | /packages/deploy-site/ 32 | .npmrc 33 | -------------------------------------------------------------------------------- /packages/test-app/app/components/use-machine.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { useMachine, matchesState } from 'ember-statecharts'; 3 | import toggleMachine from '../machines/toggle-machine'; 4 | import { action } from '@ember/object'; 5 | 6 | export default class UseMachineTest extends Component { 7 | statechart = useMachine(this, () => { 8 | return { 9 | machine: toggleMachine, 10 | }; 11 | }); 12 | 13 | @matchesState('on') isOn; 14 | @matchesState('off') isOff; 15 | 16 | @action toggle() { 17 | this.statechart.send('TOGGLE'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/config/ember-cli-update.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "1.0.0", 3 | "packages": [ 4 | { 5 | "name": "ember-cli", 6 | "version": "3.28.4", 7 | "blueprints": [ 8 | { 9 | "name": "addon", 10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output", 11 | "codemodsSource": "ember-addon-codemods-manifest@1", 12 | "isBaseBlueprint": true, 13 | "options": [ 14 | "--yarn", 15 | "--no-welcome" 16 | ] 17 | } 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /packages/site/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /*.tsbuildinfo 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 | 29 | .DS_Store 30 | # generated api-docs 31 | /public/api-docs/ 32 | -------------------------------------------------------------------------------- /packages/site/app/controllers/docs.ts: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { tracked } from '@glimmer/tracking'; 3 | import { action } from '@ember/object'; 4 | 5 | export default class Docs extends Controller { 6 | @tracked currentHeadingId: string | undefined; 7 | 8 | @action 9 | setCurrentHeadingId(id: string | undefined): void { 10 | this.currentHeadingId = id; 11 | } 12 | } 13 | 14 | // DO NOT DELETE: this is how TypeScript knows how to look up your controllers. 15 | declare module '@ember/controller' { 16 | interface Registry { 17 | docs: Docs; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/ember-statecharts/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /**/dist/ 5 | /**/tmp/ 6 | 7 | # dependencies 8 | /**/bower_components/ 9 | /**/node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /.eslintcache 16 | /*.tsbuildinfo 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 | 29 | .DS_Store 30 | 31 | /packages/deploy-site/ 32 | -------------------------------------------------------------------------------- /packages/site/app/components/page-headings.ts: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import scrollTo, { scrollToElement } from '../utils/scroll-to'; 3 | import { action } from '@ember/object'; 4 | 5 | interface PageHeadingsArgs {} //eslint-disable-line 6 | 7 | export default class PageHeadings extends Component { 8 | @action onClick(evt: MouseEvent): void { 9 | const href = (evt.target as HTMLElement).getAttribute('href'); 10 | if (href) { 11 | const toElement = document.querySelector(href) as HTMLElement; 12 | 13 | scrollToElement(toElement); 14 | } 15 | } 16 | 17 | @action scrollToTop(): void { 18 | scrollTo(0); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/ember-statecharts/.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintcache 14 | /.eslintignore 15 | /.eslintrc.js 16 | /.git/ 17 | /.gitignore 18 | /.prettierignore 19 | /.prettierrc.js 20 | /.template-lintrc.js 21 | /.travis.yml 22 | /.watchmanconfig 23 | /bower.json 24 | /config/ember-try.js 25 | /CONTRIBUTING.md 26 | /ember-cli-build.js 27 | /testem.js 28 | /tests/ 29 | /yarn-error.log 30 | /yarn.lock 31 | .gitkeep 32 | 33 | # ember-try 34 | /.node_modules.ember-try/ 35 | /bower.json.ember-try 36 | /package.json.ember-try 37 | /config/addon-docs.js 38 | -------------------------------------------------------------------------------- /packages/site/testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/test-app/testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/ember-statecharts/testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd my-addon` 7 | * `yarn` 8 | 9 | ## Linting 10 | 11 | * `yarn run lint:hbs` 12 | * `yarn run lint:js` 13 | * `yarn run lint:js -- --fix` 14 | 15 | ## Running tests 16 | 17 | * `ember test` – Runs the test suite on the current Ember version 18 | * `ember test --server` – Runs the test suite in "watch mode" 19 | * `ember try:each` – Runs the test suite against multiple Ember versions 20 | 21 | ## Running the dummy application 22 | 23 | * `ember serve` 24 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 25 | 26 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 27 | -------------------------------------------------------------------------------- /packages/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 | // Ember's browser support policy is changing, and IE11 support will end in 10 | // v4.0 onwards. 11 | // 12 | // See https://deprecations.emberjs.com/v3.x#toc_3-0-browser-support-policy 13 | // 14 | // If you need IE11 support on a version of Ember that still offers support 15 | // for it, uncomment the code block below. 16 | // 17 | // const isCI = Boolean(process.env.CI); 18 | // const isProduction = process.env.EMBER_ENV === 'production'; 19 | // 20 | // if (isCI || isProduction) { 21 | // browsers.push('ie 11'); 22 | // } 23 | 24 | module.exports = { 25 | browsers, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/ember-statecharts/ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 3 | 4 | module.exports = function (defaults) { 5 | let app = new EmberAddon(defaults, { 6 | // Add options here 7 | }); 8 | 9 | /* 10 | This build file specifies the options for the dummy test app of this 11 | addon, located in `/tests/dummy` 12 | This build file does *not* influence how the addon or the app using it 13 | behave. You most likely want to be modifying `./index.js` or app's build file 14 | */ 15 | 16 | const { maybeEmbroider } = require('@embroider/test-setup'); 17 | return maybeEmbroider(app, { 18 | skipBabel: [ 19 | { 20 | package: 'qunit', 21 | }, 22 | ], 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/site/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 21 | 22 | {{content-for "body-footer"}} 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/ember-statecharts/-private/use-resource.d.ts: -------------------------------------------------------------------------------- 1 | import type { Resource } from './resource'; 2 | interface TemplateArgs { 3 | positional: readonly unknown[]; 4 | named: Record; 5 | } 6 | declare type Args = 7 | | TemplateArgs 8 | | TemplateArgs['positional'] 9 | | TemplateArgs['named']; 10 | export declare function useUnproxiedResource< 11 | TArgs = Args, 12 | T extends Resource = Resource 13 | >( 14 | destroyable: object, 15 | definition: object, 16 | args?: () => TArgs 17 | ): { 18 | value: T; 19 | }; 20 | export declare function useResource< 21 | TArgs = Args, 22 | T extends Resource = Resource 23 | >(destroyable: object, definition: object, args?: () => TArgs): T; 24 | export {}; 25 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/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 | // Ember's browser support policy is changing, and IE11 support will end in 10 | // v4.0 onwards. 11 | // 12 | // See https://deprecations.emberjs.com/v3.x#toc_3-0-browser-support-policy 13 | // 14 | // If you need IE11 support on a version of Ember that still offers support 15 | // for it, uncomment the code block below. 16 | // 17 | // const isCI = Boolean(process.env.CI); 18 | // const isProduction = process.env.EMBER_ENV === 'production'; 19 | // 20 | // if (isCI || isProduction) { 21 | // browsers.push('ie 11'); 22 | // } 23 | 24 | module.exports = { 25 | browsers, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 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 | 21 | 22 | {{content-for "body-footer"}} 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/site/app/components/docs/quickstart-guide.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { tracked } from '@glimmer/tracking'; 3 | 4 | import { action } from '@ember/object'; 5 | 6 | import { task, timeout } from 'ember-concurrency'; 7 | 8 | export default class extends Component { 9 | // BEGIN-SNIPPET quickstart-button-used 10 | 11 | // ... 12 | @tracked 13 | failRequest = false; 14 | 15 | @(task(function* () { 16 | yield timeout(1000); 17 | 18 | if (this.failRequest) { 19 | throw 'wat'; 20 | } 21 | }).drop()) 22 | submitTask; 23 | 24 | @action 25 | onSuccess() { 26 | window.alert('Submit successful'); 27 | } 28 | 29 | @action 30 | onError() { 31 | window.alert('Submit failed'); 32 | } 33 | // ... 34 | // END-SNIPPET 35 | } 36 | -------------------------------------------------------------------------------- /packages/test-app/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TestApp 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /packages/site/app/components/counter.hbs: -------------------------------------------------------------------------------- 1 |
2 | Counter Count: {{this.statechart.state.context.count}} 3 |
4 |
5 |
6 | {{#if this.isActive}} 7 | 8 | Deactivate 9 | 10 | {{else}} 11 | 12 | Activate 13 | 14 | {{/if}} 15 |
16 |
17 | 21 | - 22 | 23 | 27 | + 28 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /packages/site/app/components/docs/typescript-usage.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | import { tracked } from '@glimmer/tracking'; 4 | import { task, timeout } from 'ember-concurrency'; 5 | 6 | export default class extends Component { 7 | @tracked 8 | failRequest = false; 9 | 10 | @tracked 11 | disabled = false; 12 | 13 | // BEGIN-SNIPPET typescript-usage 14 | 15 | // ... 16 | 17 | @(task(function* () { 18 | yield timeout(1000); 19 | 20 | if (this.failRequest) { 21 | throw 'wat'; 22 | } 23 | }).drop()) 24 | submitTask; 25 | 26 | @action 27 | onSuccess() { 28 | window.alert('Submit successful'); 29 | } 30 | 31 | @action 32 | onError() { 33 | window.alert('Submit failed'); 34 | } 35 | // ... 36 | // END-SNIPPET 37 | } 38 | -------------------------------------------------------------------------------- /packages/site/app/components/counter-restart.hbs: -------------------------------------------------------------------------------- 1 |
2 | Counter Count: {{this.statechart.state.context.count}} 3 |
4 |
5 |
6 | {{#if this.isActive}} 7 | 8 | Deactivate 9 | 10 | {{else}} 11 | 12 | Activate 13 | 14 | {{/if}} 15 |
16 |
17 | 21 | - 22 | 23 | 27 | + 28 | 29 |
30 |
31 | -------------------------------------------------------------------------------- /packages/site/public/effective-ember.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/site/app/components/docs/quickstart-final.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | import { tracked } from '@glimmer/tracking'; 4 | import { task, timeout } from 'ember-concurrency'; 5 | 6 | export default class extends Component { 7 | @tracked 8 | failRequest = false; 9 | 10 | @tracked 11 | disabled = false; 12 | 13 | // BEGIN-SNIPPET quickstart-button-final-used 14 | 15 | // ... 16 | 17 | @(task(function* () { 18 | yield timeout(1000); 19 | 20 | if (this.failRequest) { 21 | throw 'wat'; 22 | } 23 | }).drop()) 24 | submitTask; 25 | 26 | @action 27 | onSuccess() { 28 | window.alert('Submit successful'); 29 | } 30 | 31 | @action 32 | onError() { 33 | window.alert('Submit failed'); 34 | } 35 | // ... 36 | // END-SNIPPET 37 | } 38 | -------------------------------------------------------------------------------- /packages/site/app/components/mobile-docs-toc.hbs: -------------------------------------------------------------------------------- 1 |
2 | 23 | {{#if this.showToc}} 24 |
25 | 26 | 27 |
28 | {{/if}} 29 |
30 | -------------------------------------------------------------------------------- /packages/site/.docfy-config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const autolinkHeadings = require('remark-autolink-headings'); 3 | const docfyWithProse = require('@docfy/plugin-with-prose'); 4 | const prism = require('@mapbox/rehype-prism'); 5 | const refractor = require('refractor'); 6 | 7 | refractor.alias('handlebars', 'hbs'); 8 | refractor.alias('shell', 'sh'); 9 | 10 | module.exports = { 11 | tocMaxDepth: 3, 12 | sources: [ 13 | { 14 | root: path.resolve(__dirname, '../../docs'), 15 | pattern: '**/*.md', 16 | urlPrefix: 'docs', 17 | }, 18 | ], 19 | remarkPlugins: [ 20 | [ 21 | autolinkHeadings, 22 | { 23 | behavior: 'wrap', 24 | }, 25 | ], 26 | ], 27 | plugins: [docfyWithProse], 28 | rehypePlugins: [prism], 29 | labels: { 30 | components: 'Components', 31 | workflow: 'Workflow', 32 | docs: 'Documentation', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /packages/test-app/ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | 5 | module.exports = function (defaults) { 6 | let app = new EmberApp(defaults, { 7 | // Add options here 8 | }); 9 | 10 | // Use `app.import` to add additional libraries to the generated 11 | // output files. 12 | // 13 | // If you need to use different assets in different 14 | // environments, specify an object as the first parameter. That 15 | // object's keys should be the environment name and the values 16 | // should be the asset to use in that environment. 17 | // 18 | // If the library that you are including contains AMD or ES6 19 | // modules that you would like to import into your application 20 | // please specify an object with the list of modules as keys 21 | // along with the exports of each module as its value. 22 | 23 | return app.toTree(); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/site/app/machines/quickstart-button.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET quickstart-button-machine 2 | import { Machine } from 'xstate'; 3 | 4 | export default Machine( 5 | { 6 | initial: 'idle', 7 | states: { 8 | idle: { 9 | on: { 10 | SUBMIT: 'busy', 11 | }, 12 | }, 13 | busy: { 14 | entry: ['handleSubmit'], 15 | on: { 16 | SUCCESS: 'success', 17 | ERROR: 'error', 18 | }, 19 | }, 20 | success: { 21 | entry: ['handleSuccess'], 22 | on: { 23 | SUBMIT: 'busy', 24 | }, 25 | }, 26 | error: { 27 | entry: ['handleError'], 28 | on: { 29 | SUBMIT: 'busy', 30 | }, 31 | }, 32 | }, 33 | }, 34 | { 35 | actions: { 36 | handleSubmit() {}, 37 | handleSuccess() {}, 38 | handleError() {}, 39 | }, 40 | } 41 | ); 42 | // END-SNIPPET 43 | -------------------------------------------------------------------------------- /packages/site/public/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/site/lib/versioned-doc-info.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | const getRepoInfo = require('git-repo-info'); 5 | 6 | module.exports = function (deployTarget) { 7 | let dir, name, rootURL; 8 | let { sha, tag, branch } = getRepoInfo(); 9 | 10 | if (deployTarget === 'tag') { 11 | dir = tag; 12 | name = tag; 13 | branch = null; 14 | } else if (deployTarget === 'branch') { 15 | dir = branch; 16 | name = branch; 17 | tag = null; 18 | } else if (deployTarget === 'latest') { 19 | dir = 'latest'; 20 | name = 'Latest'; 21 | } else { 22 | throw new Error(`unsupported deploy target ${deployTarget}`); 23 | } 24 | 25 | let destDir = `versions/${dir}`; 26 | 27 | if (deployTarget === 'latest') { 28 | rootURL = '/'; 29 | } else { 30 | rootURL = `/${destDir}/`; 31 | } 32 | 33 | return { 34 | name, 35 | sha, 36 | tag, 37 | branch, 38 | destDir, 39 | rootURL, 40 | deployTarget, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/ember-statecharts/README.md: -------------------------------------------------------------------------------- 1 | ember-statecharts 2 | ============================================================================== 3 | 4 | [Short description of the addon.] 5 | 6 | 7 | Compatibility 8 | ------------------------------------------------------------------------------ 9 | 10 | * Ember.js v3.20 or above 11 | * Ember CLI v3.20 or above 12 | * Node.js v12 or above 13 | 14 | 15 | Installation 16 | ------------------------------------------------------------------------------ 17 | 18 | ``` 19 | ember install ember-statecharts 20 | ``` 21 | 22 | 23 | Usage 24 | ------------------------------------------------------------------------------ 25 | 26 | [Longer description of how to use the addon in apps.] 27 | 28 | 29 | Contributing 30 | ------------------------------------------------------------------------------ 31 | 32 | See the [Contributing](CONTRIBUTING.md) guide for details. 33 | 34 | 35 | License 36 | ------------------------------------------------------------------------------ 37 | 38 | This project is licensed under the [MIT License](LICENSE.md). 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 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 | -------------------------------------------------------------------------------- /packages/site/app/machines/counter-machine.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET counter-machine 2 | import { Machine, assign } from 'xstate'; 3 | 4 | export default Machine({ 5 | id: 'counterMachine', 6 | initial: 'inactive', 7 | context: { 8 | count: 0, 9 | }, 10 | on: { 11 | RESET_COUNT: { 12 | actions: [ 13 | assign({ 14 | count: (_context, { count }) => count, 15 | }), 16 | ], 17 | }, 18 | }, 19 | states: { 20 | inactive: { 21 | on: { 22 | ACTIVATE: 'active', 23 | }, 24 | }, 25 | active: { 26 | on: { 27 | DEACTIVATE: 'inactive', 28 | INCREMENT: { 29 | target: 'active', 30 | actions: [ 31 | assign({ 32 | count: (context) => context.count + 1, 33 | }), 34 | ], 35 | }, 36 | DECREMENT: { 37 | target: 'active', 38 | actions: [ 39 | assign({ 40 | count: (context) => context.count - 1, 41 | }), 42 | ], 43 | }, 44 | }, 45 | }, 46 | }, 47 | }); 48 | // END-SNIPPET 49 | -------------------------------------------------------------------------------- /packages/site/app/components/counter-restart.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET counter-update-restart 2 | import Component from '@glimmer/component'; 3 | import { action } from '@ember/object'; 4 | import { useMachine, matchesState } from 'ember-statecharts'; 5 | import CounterMachine from '../machines/counter-machine'; 6 | 7 | export default class CounterComponent extends Component { 8 | statechart = useMachine(this, () => { 9 | return { 10 | machine: CounterMachine.withContext({ 11 | count: this.args.count, 12 | }), 13 | update: ({ restart }) => { 14 | restart(); 15 | }, 16 | }; 17 | }); 18 | 19 | @matchesState('active') 20 | isActive; 21 | 22 | @matchesState('inactive') 23 | isDisabled; 24 | 25 | @action 26 | decrement() { 27 | this.statechart.send('DECREMENT'); 28 | } 29 | 30 | @action 31 | increment() { 32 | this.statechart.send('INCREMENT'); 33 | } 34 | 35 | @action 36 | activate() { 37 | this.statechart.send('ACTIVATE'); 38 | } 39 | 40 | @action 41 | deactivate() { 42 | this.statechart.send('DEACTIVATE'); 43 | } 44 | } 45 | // END-SNIPPET 46 | -------------------------------------------------------------------------------- /packages/site/app/components/docs-toc.hbs: -------------------------------------------------------------------------------- 1 | 2 |
    3 | {{#each node.pages as |page|}} 4 |
  • 5 | 11 | {{page.title}} 12 | 13 |
  • 14 | {{/each}} 15 | 16 | {{#each node.children as |child|}} 17 |
  • 18 |
    19 | {{child.label}} 20 |
    21 |
      22 | {{#each child.pages as |page|}} 23 |
    • 24 | 29 | {{page.title}} 30 | 31 |
    • 32 | {{/each}} 33 |
    34 |
  • 35 | {{/each}} 36 |
37 |
38 | -------------------------------------------------------------------------------- /packages/site/public/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/site/app/routes/application.ts: -------------------------------------------------------------------------------- 1 | import Route from '@ember/routing/route'; 2 | import RouterService from '@ember/routing/router-service'; 3 | import { inject as service } from '@ember/service'; 4 | import config from 'site/config/environment'; 5 | import { action } from '@ember/object'; 6 | 7 | import MetricsService from 'ember-metrics/services/metrics'; 8 | 9 | export default class ApplicationRoute extends Route { 10 | @service 11 | router!: RouterService; 12 | 13 | @service 14 | metrics!: MetricsService; 15 | 16 | constructor() { 17 | // eslint-disable-next-line prefer-rest-params 18 | super(...arguments); 19 | 20 | this.setupTracking(); 21 | } 22 | 23 | @action 24 | didTransition(): void { 25 | if ( 26 | config.environment !== 'test' && 27 | window && 28 | typeof window.scrollTo === 'function' 29 | ) { 30 | window.scrollTo(0, 0); 31 | } 32 | } 33 | 34 | private setupTracking() { 35 | this.router.on('routeDidChange', () => { 36 | const { currentURL, currentRouteName } = this.router; 37 | 38 | this.metrics.trackPage({ 39 | page: currentURL, 40 | title: currentRouteName, 41 | }); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/site/app/components/counter.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET counter-update-event 2 | import Component from '@glimmer/component'; 3 | import { action } from '@ember/object'; 4 | import { matchesState } from 'ember-statecharts'; 5 | import { useMachine } from 'ember-statecharts/-private/usables'; 6 | import CounterMachine from '../machines/counter-machine'; 7 | 8 | export default class CounterComponent extends Component { 9 | statechart = useMachine(this, () => { 10 | return { 11 | machine: CounterMachine.withContext({ 12 | count: this.args.count, 13 | }), 14 | update: ({ send, machine: { context } }) => { 15 | send('RESET_COUNT', { count: context.count }); 16 | }, 17 | }; 18 | }); 19 | 20 | @matchesState('active') 21 | isActive; 22 | 23 | @matchesState('inactive') 24 | isDisabled; 25 | 26 | @action 27 | decrement() { 28 | this.statechart.send('DECREMENT'); 29 | } 30 | 31 | @action 32 | increment() { 33 | this.statechart.send('INCREMENT'); 34 | } 35 | 36 | @action 37 | activate() { 38 | this.statechart.send('ACTIVATE'); 39 | } 40 | 41 | @action 42 | deactivate() { 43 | this.statechart.send('DEACTIVATE'); 44 | } 45 | } 46 | // END-SNIPPET 47 | -------------------------------------------------------------------------------- /packages/site/public/xstate.svg: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "allowJs": true, 5 | "moduleResolution": "node", 6 | "allowSyntheticDefaultImports": true, 7 | "noImplicitAny": true, 8 | "noImplicitThis": true, 9 | "alwaysStrict": true, 10 | "strictNullChecks": true, 11 | "strictPropertyInitialization": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noEmitOnError": false, 17 | "noEmit": true, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "baseUrl": ".", 21 | "module": "es6", 22 | "experimentalDecorators": true, 23 | "paths": { 24 | "dummy/tests/*": [ 25 | "tests/*" 26 | ], 27 | "dummy/*": [ 28 | "tests/dummy/app/*", 29 | "app/*" 30 | ], 31 | "ember-statecharts": [ 32 | "addon" 33 | ], 34 | "ember-statecharts/*": [ 35 | "addon/*" 36 | ], 37 | "ember-statecharts/test-support": [ 38 | "addon-test-support" 39 | ], 40 | "ember-statecharts/test-support/*": [ 41 | "addon-test-support/*" 42 | ], 43 | "*": [ 44 | "types/*" 45 | ] 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/site/app/components/docs/quickstart-guide.hbs: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{! BEGIN-SNIPPET quickstart-button-used }} 4 | 9 | Click me! 10 | 11 | {{! END-SNIPPET }} 12 |
13 | 16 | 23 |
24 |
25 | 29 | 33 | 37 | 41 | 45 |
46 | -------------------------------------------------------------------------------- /packages/site/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{content-for "head"}} 10 | {{content-for "test-head"}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | {{content-for "test-head-footer"}} 18 | 19 | 20 | {{content-for "body"}} 21 | {{content-for "test-body"}} 22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {{content-for "body-footer"}} 37 | {{content-for "test-body-footer"}} 38 | 39 | 40 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{content-for "head"}} 10 | {{content-for "test-head"}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} 17 | {{content-for "test-head-footer"}} 18 | 19 | 20 | {{content-for "body"}} 21 | {{content-for "test-body"}} 22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {{content-for "body-footer"}} 37 | {{content-for "test-body-footer"}} 38 | 39 | 40 | -------------------------------------------------------------------------------- /packages/site/app/components/demo.hbs: -------------------------------------------------------------------------------- 1 | 2 |
6 |
7 | {{yield (hash ui=(hash useSnippet=showcase.ui.useSnippet))}} 8 |
9 |
10 |
11 | {{#each showcase.state.snippets as |snippet|}} 12 | 28 | {{/each}} 29 |
30 | {{#if showcase.state.activeSnippet}} 31 | 36 | {{/if}} 37 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /packages/test-app/tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | TestApp Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {{content-for "body-footer"}} 38 | {{content-for "test-body-footer"}} 39 | 40 | 41 | -------------------------------------------------------------------------------- /packages/site/app/components/docs/typescript-usage.hbs: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{! BEGIN-SNIPPET typed-button-used }} 4 | 10 | .ts FTW 11 | 12 | {{! END-SNIPPET }} 13 |
14 | 17 | 24 |
25 |
26 | 29 | 36 |
37 |
38 | 39 | 40 | 41 | 42 |
43 | -------------------------------------------------------------------------------- /packages/site/ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const crawl = require('prember-crawler'); 3 | const EmberApp = require('ember-cli/lib/broccoli/ember-app'); 4 | 5 | module.exports = function (defaults) { 6 | let app = new EmberApp(defaults, { 7 | // Add options here 8 | postcssOptions: { 9 | compile: { 10 | plugins: [ 11 | require('postcss-import'), 12 | require('tailwindcss')('./tailwind.config.js'), 13 | require('autoprefixer'), 14 | ], 15 | }, 16 | }, 17 | snippetPaths: ['app'], 18 | snippetSearchPaths: ['app'], //, '../../docs'], 19 | 'ember-prism': { 20 | theme: 'tomorrow', 21 | components: ['markup-templating', 'handlebars', 'typescript'], 22 | }, 23 | prember: { 24 | urls: async ({ visit }) => { 25 | let docsURLs = await crawl({ 26 | visit, 27 | startingFrom: ['/docs'], 28 | selector: 'a[data-prember]', 29 | }); 30 | 31 | let otherURLS = ['/', '/docs']; 32 | 33 | return docsURLs.concat(otherURLS); 34 | }, 35 | }, 36 | }); 37 | 38 | /* 39 | This build file specifies the options for the dummy test app of this 40 | addon, located in `/tests/dummy` 41 | This build file does *not* influence how the addon or the app using it 42 | behave. You most likely want to be modifying `./index.js` or app's build file 43 | */ 44 | 45 | return app.toTree(); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/site/app/utils/scroll-to.ts: -------------------------------------------------------------------------------- 1 | // http://goo.gl/5HLl8 2 | const easeInOutQuad = (t: number, b: number, c: number, d: number): number => { 3 | t /= d / 2; 4 | if (t < 1) { 5 | return (c / 2) * t * t + b; 6 | } 7 | t--; 8 | return (-c / 2) * (t * (t - 2) - 1) + b; 9 | }; 10 | 11 | function scrollTo( 12 | toPosition: number, 13 | callback?: () => void, 14 | duration = 500 15 | ): void { 16 | const scrollingElement = document.scrollingElement 17 | ? document.scrollingElement 18 | : document.body; 19 | const startPosition = scrollingElement.scrollTop; 20 | const change = toPosition - startPosition; 21 | let currentTime = 0; 22 | const increment = 20; 23 | 24 | const animateScroll = (): void => { 25 | currentTime += increment; 26 | scrollingElement.scrollTop = easeInOutQuad( 27 | currentTime, 28 | startPosition, 29 | change, 30 | duration 31 | ); 32 | 33 | if (currentTime < duration) { 34 | requestAnimationFrame(animateScroll); 35 | } else { 36 | if (callback && typeof callback === 'function') { 37 | callback(); 38 | } 39 | } 40 | }; 41 | animateScroll(); 42 | } 43 | 44 | function scrollToElement( 45 | element: HTMLElement, 46 | callback?: () => void, 47 | duration = 500 48 | ): void { 49 | const toPosition = element.offsetTop; 50 | scrollTo(toPosition, callback, duration); 51 | } 52 | 53 | export { scrollToElement }; 54 | export default scrollTo; 55 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | workflow_dispatch: 10 | latest: 11 | description: 'Deploy latest?' 12 | required: true 13 | type: boolean 14 | 15 | jobs: 16 | checkout: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout Code 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: '0' 23 | - name: Install Node 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: 14 27 | - name: Install Dependencies 28 | run: yarn install --frozen-lockfile 29 | - name: Lerna Bootstrap 30 | run: yarn run lerna bootstrap 31 | - name: Create API docs 32 | run: yarn workspace ember-statecharts api-docs 33 | - name: update git credentials 34 | run: | 35 | git config --global user.name "ember-statecharts deployer" 36 | git config --global user.email "urbi@orbi.vat" 37 | 38 | - name: Deploy tag 39 | if: startsWith(github.ref, 'refs/tags/v') 40 | run: yarn workspace site run ember deploy tag 41 | 42 | - name: Deploy branch 43 | if: github.ref == 'refs/heads/master' 44 | run: yarn workspace site run ember deploy branch 45 | 46 | - name: Deploy latest 47 | if: github.event.inputs.latest == 'true' 48 | run: yarn workspace site run ember deploy latest 49 | -------------------------------------------------------------------------------- /packages/test-app/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (environment) { 4 | let ENV = { 5 | modulePrefix: 'test-app', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false, 17 | }, 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /packages/test-app/tests/acceptance/smoke-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { visit, click, find } from '@ember/test-helpers'; 3 | import { setupApplicationTest } from 'ember-qunit'; 4 | 5 | class TogglePage { 6 | constructor(scope) { 7 | this.scope = scope; 8 | } 9 | 10 | get content() { 11 | return find(`${this.scope} [data-test-content]`); 12 | } 13 | 14 | toggle() { 15 | return click(`${this.scope} [data-test-toggle-button]`); 16 | } 17 | } 18 | 19 | async function runToggleTest(page, assert) { 20 | assert.dom(page.content).hasText('Off', 'machine is off by default'); 21 | 22 | await page.toggle(); 23 | 24 | assert.dom(page.content).hasText('On', 'toggling turns machine on'); 25 | 26 | await page.toggle(); 27 | 28 | assert 29 | .dom(page.content) 30 | .hasText('Off', 'toggling again turns machine off again'); 31 | } 32 | 33 | module('Acceptance | smoke', function (hooks) { 34 | setupApplicationTest(hooks); 35 | 36 | test('@statechart computed api works', async function (assert) { 37 | assert.expect(3); 38 | 39 | await visit('/'); 40 | 41 | const page = new TogglePage('[data-test-computed]'); 42 | 43 | await runToggleTest(page, assert); 44 | }); 45 | 46 | test('use-machine api works', async function (assert) { 47 | assert.expect(3); 48 | 49 | await visit('/'); 50 | 51 | const page = new TogglePage('[data-test-use-machine]'); 52 | 53 | await runToggleTest(page, assert); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/test-app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | legacyDecorators: true, 11 | }, 12 | }, 13 | plugins: ['ember'], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:ember/recommended', 17 | 'plugin:prettier/recommended', 18 | ], 19 | env: { 20 | browser: true, 21 | }, 22 | rules: {}, 23 | overrides: [ 24 | // node files 25 | { 26 | files: [ 27 | './.eslintrc.js', 28 | './.prettierrc.js', 29 | './.template-lintrc.js', 30 | './ember-cli-build.js', 31 | './testem.js', 32 | './blueprints/*/index.js', 33 | './config/**/*.js', 34 | './lib/*/index.js', 35 | './server/**/*.js', 36 | ], 37 | parserOptions: { 38 | sourceType: 'script', 39 | }, 40 | env: { 41 | browser: false, 42 | node: true, 43 | }, 44 | plugins: ['node'], 45 | extends: ['plugin:node/recommended'], 46 | rules: { 47 | // this can be removed once the following is fixed 48 | // https://github.com/mysticatea/eslint-plugin-node/issues/77 49 | 'node/no-unpublished-require': 'off', 50 | }, 51 | }, 52 | { 53 | // Test files: 54 | files: ['tests/**/*-test.{js,ts}'], 55 | extends: ['plugin:qunit/recommended'], 56 | }, 57 | ], 58 | }; 59 | -------------------------------------------------------------------------------- /packages/ember-statecharts/addon/utils/statechart.js: -------------------------------------------------------------------------------- 1 | import { Machine, interpret } from 'xstate'; 2 | import { set } from '@ember/object'; 3 | import { later, cancel } from '@ember/runloop'; 4 | import { warn } from '@ember/debug'; 5 | 6 | export default class Statechart { 7 | constructor(config, options, initialContext) { 8 | const machine = Machine(config, options, initialContext); 9 | this.service = interpret(machine, { 10 | clock: { 11 | setTimeout: (fn, ms) => { 12 | return later.call(null, fn, ms); 13 | }, 14 | clearTimeout: (timer) => { 15 | return cancel.call(null, timer); 16 | }, 17 | }, 18 | }) 19 | .onTransition((state) => { 20 | set(this, 'currentState', state); 21 | }) 22 | .start(); 23 | } 24 | 25 | send(event, data = {}) { 26 | if (arguments.length === 1) { 27 | this._sendEventObject(event); 28 | } else { 29 | const { type, ...eventData } = data; 30 | if (type) { 31 | warn( 32 | `You passed property \`type\` as part of the data you sent with the event \`${event}\` . This is not supported - \`${event}\` will be used as event name.`, 33 | false, 34 | { id: 'statecharts.event-object.no-override-type' } 35 | ); 36 | } 37 | const eventObject = { ...eventData, type: event }; 38 | 39 | this._sendEventObject(eventObject); 40 | } 41 | } 42 | 43 | _sendEventObject(eventObject) { 44 | this.service.send(eventObject); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/site/app/components/ui-intro.md: -------------------------------------------------------------------------------- 1 | # Motivation 2 | 3 | `ember-statecharts` provides an easy way to use statecharts in ember applications. 4 | This is especially useful in `Ember.Component`-architecture but can be used across all layers of your application - e.g. when implementing global application state backed by an `Ember.Service` that needs to switch your application into a specifc mode based on data that your application receives via push events. 5 | 6 | By using statecharts you can improve your development workflow in multiple ways: 7 | 8 | * Model behavior explicitly. Instead of relying on implicit states that you manage by setting properties on your objects you will model your behavior as a set of explicit `state`s that handle events. 9 | * Create robust applications that won't break. Because behavior is only executed when a given state understands an event that is being triggered it is impossible to trigger invalid or unexpected application behavior. 10 | * Refactor and refine with confidence. Application flows modeled with statecharts are easy to change without the risk of breaking existing behavior. 11 | * Document behavior. Because statecharts can be visualized you finally have a way of communicating about application behavior with stakeholders that don't have a programming background. It will also help fellow developers to reason about behavior in your application. 12 | * Identify missing requirements. Because you make use of a visual language that describes behavior it gets very easy to identify holes in requirement documents. -------------------------------------------------------------------------------- /packages/site/app/components/docs/quickstart-final.hbs: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{! BEGIN-SNIPPET quickstart-button-final-used }} 4 | 10 | Click me! 11 | 12 | {{! END-SNIPPET }} 13 |
14 | 17 | 24 |
25 |
26 | 29 | 36 |
37 |
38 | 42 | 46 | 50 | 54 |
55 | -------------------------------------------------------------------------------- /packages/test-app/README.md: -------------------------------------------------------------------------------- 1 | # test-app 2 | 3 | This README outlines the details of collaborating on this Ember application. 4 | A short introduction of this app could easily go here. 5 | 6 | ## Prerequisites 7 | 8 | You will need the following things properly installed on your computer. 9 | 10 | * [Git](https://git-scm.com/) 11 | * [Node.js](https://nodejs.org/) 12 | * [Yarn](https://yarnpkg.com/) 13 | * [Ember CLI](https://ember-cli.com/) 14 | * [Google Chrome](https://google.com/chrome/) 15 | 16 | ## Installation 17 | 18 | * `git clone ` this repository 19 | * `cd test-app` 20 | * `yarn install` 21 | 22 | ## Running / Development 23 | 24 | * `ember serve` 25 | * Visit your app at [http://localhost:4200](http://localhost:4200). 26 | * Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests). 27 | 28 | ### Code Generators 29 | 30 | Make use of the many generators for code, try `ember help generate` for more details 31 | 32 | ### Running Tests 33 | 34 | * `ember test` 35 | * `ember test --server` 36 | 37 | ### Linting 38 | 39 | * `yarn lint` 40 | * `yarn lint:fix` 41 | 42 | ### Building 43 | 44 | * `ember build` (development) 45 | * `ember build --environment production` (production) 46 | 47 | ### Deploying 48 | 49 | Specify what it takes to deploy your app. 50 | 51 | ## Further Reading / Useful Links 52 | 53 | * [ember.js](https://emberjs.com/) 54 | * [ember-cli](https://ember-cli.com/) 55 | * Development Browser Extensions 56 | * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) 57 | * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) 58 | -------------------------------------------------------------------------------- /packages/ember-statecharts/addon/computed.js: -------------------------------------------------------------------------------- 1 | import { computed, get } from '@ember/object'; 2 | import { matchesState as xstateMatchesState } from 'xstate'; 3 | import { A, makeArray } from '@ember/array'; 4 | import Statechart from './utils/statechart'; 5 | 6 | function decorateStopInterpreterOnDestroy(destroyFn, service) { 7 | return function () { 8 | service.stop(); 9 | 10 | destroyFn.apply(this, ...arguments); 11 | }; 12 | } 13 | 14 | function matchesState(states, statechartPropertyName = 'statechart') { 15 | return computed(`${statechartPropertyName}.currentState`, function () { 16 | const _states = A(makeArray(states)); 17 | 18 | return _states.any((state) => { 19 | return xstateMatchesState( 20 | state, 21 | get(this, `${statechartPropertyName}.currentState.value`) 22 | ); 23 | }); 24 | }); 25 | } 26 | 27 | function debugState(statechartPropertyName = 'statechart') { 28 | return computed(`${statechartPropertyName}.currentState`, function () { 29 | return JSON.stringify( 30 | get(this, `${statechartPropertyName}.currentState.value`) 31 | ); 32 | }); 33 | } 34 | 35 | function statechart(config, options) { 36 | /* eslint-disable ember/require-computed-property-dependencies */ 37 | return computed(function () { 38 | const initialContext = this; 39 | 40 | const statechart = new Statechart(config, options, initialContext); 41 | 42 | /* eslint-disable ember/no-side-effects */ 43 | this.willDestroy = decorateStopInterpreterOnDestroy( 44 | this.willDestroy, 45 | statechart.service 46 | ); 47 | 48 | return statechart; 49 | }); 50 | } 51 | 52 | export { statechart, matchesState, debugState }; 53 | -------------------------------------------------------------------------------- /packages/site/app/styles/app.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss/base"; 2 | @import "tailwindcss/components"; 3 | @import "tailwindcss/utilities"; 4 | 5 | .w-128 { 6 | width: 32rem; 7 | } 8 | 9 | .h-128 { 10 | height: 32rem; 11 | } 12 | 13 | .lds-ring { 14 | display: inline-block; 15 | position: relative; 16 | width: 16px; 17 | height: 16px; 18 | } 19 | 20 | .lds-ring div { 21 | box-sizing: border-box; 22 | display: block; 23 | position: absolute; 24 | width: 16px; 25 | height: 16px; 26 | border: 2px solid #fff; 27 | border-radius: 50%; 28 | animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 29 | border-color: #fff transparent transparent transparent; 30 | } 31 | .lds-ring div:nth-child(1) { 32 | animation-delay: -0.45s; 33 | } 34 | .lds-ring div:nth-child(2) { 35 | animation-delay: -0.3s; 36 | } 37 | .lds-ring div:nth-child(3) { 38 | animation-delay: -0.15s; 39 | } 40 | @keyframes lds-ring { 41 | 0% { 42 | transform: rotate(0deg); 43 | } 44 | 100% { 45 | transform: rotate(360deg); 46 | } 47 | } 48 | 49 | .quote { 50 | @apply flex; 51 | } 52 | .quote:before { 53 | content: ""; 54 | } 55 | 56 | .quote:after { 57 | content: ""; 58 | } 59 | 60 | .fancy-underline { 61 | @apply inline-block relative 62 | 63 | } 64 | .fancy-underline::before { 65 | content: ''; 66 | left: 50%; 67 | @apply bg-current absolute bottom-0 w-0 h-0.5 transform transition-all ease-in-out 68 | } 69 | 70 | .fancy-underline:hover::before { 71 | @apply w-full left-0 72 | } 73 | 74 | .text-underline-under { 75 | text-underline-position: under; 76 | } 77 | 78 | /* for easier styling of demo blocks - prism and prose will be overidden */ 79 | pre[class*="language-"] { 80 | margin: 0; 81 | } 82 | -------------------------------------------------------------------------------- /packages/ember-statecharts/index.d.ts: -------------------------------------------------------------------------------- 1 | import { useMachine } from './-private/usables'; 2 | import type { StateValue } from 'xstate'; 3 | /** 4 | * A decorator that can be used to create a getter that matches against an 5 | * {@link Statechart}'s state and will return either `true` or `false` 6 | * 7 | * 8 | * ```js 9 | * import Component from '@glimmer/component'; 10 | * import buttonMachine from '../machines/button'; 11 | * 12 | * export default class Button extends Component { 13 | * @use statechart = useMachine(this, () => { 14 | * return { 15 | * machine: buttonMachine 16 | * } 17 | * }) 18 | * 19 | * @matchesState('disabled') 20 | * isDisabled; // true when statechart is in `disabled` state 21 | * 22 | * @matchesState({ interactivity: 'disabled' }) 23 | * isDisabled; // it is possible to match against nested/parallel states 24 | * } 25 | * ``` 26 | * 27 | * You can match against any XState [StateValue](https://xstate.js.org/api/globals.html#statevalue) 28 | * 29 | * By default `matchesState` expects your {@link Statechart} to be called `statechart`. 30 | * If you named it differently you can use the second param to this decorator: 31 | * 32 | * 33 | * ```js 34 | * import Component from '@glimmer/component'; 35 | * import buttonMachine from '../machines/button'; 36 | * 37 | * export default class Button extends Component { 38 | * @use sc = useMachine(this, () => { 39 | * return { 40 | * machine: buttonMachine 41 | * } 42 | * }) 43 | * 44 | * @matchesState('disabled', 'sc') 45 | * isDisabled; 46 | * } 47 | * ``` 48 | * 49 | */ 50 | declare function matchesState( 51 | state: StateValue, 52 | statechartPropertyName?: string 53 | ): any; 54 | export { useMachine, matchesState }; 55 | -------------------------------------------------------------------------------- /packages/site/app/components/api-toc.hbs: -------------------------------------------------------------------------------- 1 | 57 | -------------------------------------------------------------------------------- /packages/site/app/components/quickstart-button.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET quickstart-button 2 | import Component from '@glimmer/component'; 3 | import { action } from '@ember/object'; 4 | import { task } from 'ember-concurrency'; 5 | import { matchesState } from 'ember-statecharts'; 6 | import { useMachine } from 'ember-statecharts/-private/usables'; 7 | 8 | import quickstartButtonMachine from '../machines/quickstart-button'; 9 | 10 | function noop() {} 11 | 12 | export default class QuickstartButton extends Component { 13 | get onClick() { 14 | return this.args.onClick || noop; 15 | } 16 | 17 | statechart = useMachine(this, () => { 18 | const { performSubmitTask, onSuccess, onError } = this; 19 | 20 | return { 21 | machine: quickstartButtonMachine.withConfig({ 22 | actions: { 23 | handleSubmit: performSubmitTask, 24 | handleSuccess: onSuccess, 25 | handleError: onError, 26 | }, 27 | }), 28 | }; 29 | }); 30 | 31 | @matchesState('busy') 32 | isBusy; 33 | 34 | get isDisabled() { 35 | return this.isBusy || this.args.disabled; 36 | } 37 | 38 | @task(function* () { 39 | try { 40 | const result = yield this.onClick(); 41 | this.statechart.send('SUCCESS', { result }); 42 | } catch (e) { 43 | this.statechart.send('ERROR', { error: e }); 44 | } 45 | }) 46 | handleSubmitTask; 47 | 48 | @action 49 | handleClick() { 50 | this.statechart.send('SUBMIT'); 51 | } 52 | 53 | @action 54 | onSuccess(_context, { result }) { 55 | return (this.args.onSuccess && this.args.onSuccess(result)) || noop(); 56 | } 57 | 58 | @action 59 | onError(_context, { error }) { 60 | return (this.args.onError && this.args.onError(error)) || noop(); 61 | } 62 | 63 | @action 64 | performSubmitTask() { 65 | this.handleSubmitTask.perform(); 66 | } 67 | } 68 | // END-SNIPPET 69 | -------------------------------------------------------------------------------- /packages/ember-statecharts/addon/-private/use-resource.ts: -------------------------------------------------------------------------------- 1 | import type { Resource } from './resource'; 2 | import type { Cache } from '@glimmer/validator'; 3 | import { invokeHelper } from '@ember/helper'; 4 | 5 | interface TemplateArgs { 6 | positional: readonly unknown[]; 7 | named: Record; 8 | } 9 | type Args = TemplateArgs | TemplateArgs['positional'] | TemplateArgs['named']; 10 | 11 | import { getValue } from '@glimmer/tracking/primitives/cache'; 12 | 13 | export function useUnproxiedResource< 14 | TArgs = Args, 15 | T extends Resource = Resource 16 | >(destroyable: object, definition: object, args?: () => TArgs): { value: T } { 17 | let resource: Cache; 18 | 19 | return { 20 | get value(): T { 21 | if (!resource) { 22 | resource = invokeHelper( 23 | destroyable, 24 | definition, // eslint-disable-line 25 | args 26 | ) as Cache; 27 | } 28 | 29 | return getValue(resource)!; // eslint-disable-line 30 | }, 31 | }; 32 | } 33 | 34 | export function useResource< 35 | TArgs = Args, 36 | T extends Resource = Resource 37 | >(destroyable: object, definition: object, args?: () => TArgs): T { 38 | const target = useUnproxiedResource(destroyable, definition, args); 39 | 40 | return new Proxy(target, { 41 | get(target, key): unknown { 42 | const instance = target.value; 43 | const value = Reflect.get(instance, key, instance); 44 | 45 | return typeof value === 'function' ? value.bind(instance) : value; 46 | }, 47 | ownKeys(target): (string | number | symbol)[] { 48 | return Reflect.ownKeys(target.value); 49 | }, 50 | getOwnPropertyDescriptor(target, key): PropertyDescriptor | undefined { 51 | return Reflect.getOwnPropertyDescriptor(target.value, key); 52 | }, 53 | }) as never as T; 54 | } 55 | -------------------------------------------------------------------------------- /.eslintrc.base.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | legacyDecorators: true, 11 | }, 12 | }, 13 | plugins: ['ember'], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:ember/recommended', 17 | 'plugin:prettier/recommended', 18 | ], 19 | env: { 20 | browser: true, 21 | }, 22 | overrides: [ 23 | // use different parser for ts files 24 | { 25 | files: ['**/*.ts'], 26 | parser: '@typescript-eslint/parser', 27 | extends: [ 28 | 'eslint:recommended', 29 | 'plugin:@typescript-eslint/recommended', 30 | 'plugin:ember/recommended', 31 | 'plugin:prettier/recommended', 32 | ], 33 | rules: { 34 | '@typescript-eslint/ban-types': ['off', { object: null }], 35 | }, 36 | }, 37 | // node files 38 | { 39 | files: [ 40 | './.eslintrc.js', 41 | './.eslintrc.base.js', 42 | './.prettierrc.js', 43 | './.template-lintrc.js', 44 | './.docfy-config.js', 45 | './tailwind.config.js', 46 | './ember-cli-build.js', 47 | './index.js', 48 | './testem.js', 49 | './blueprints/*/index.js', 50 | './config/**/*.js', 51 | './tests/dummy/config/**/*.js', 52 | ], 53 | excludedFiles: [ 54 | 'addon/**', 55 | 'addon-test-support/**', 56 | 'app/**', 57 | 'tests/dummy/app/**', 58 | ], 59 | parserOptions: { 60 | sourceType: 'script', 61 | }, 62 | env: { 63 | browser: false, 64 | node: true, 65 | }, 66 | plugins: ['node'], 67 | extends: ['plugin:node/recommended'], 68 | rules: { 69 | 'node/no-unpublished-require': 'off', 70 | 'node/no-extraneous-require': 'off', 71 | }, 72 | }, 73 | { 74 | // Test files: 75 | files: ['tests/**/*-test.{js,ts}'], 76 | extends: ['plugin:qunit/recommended'], 77 | }, 78 | ], 79 | }; 80 | -------------------------------------------------------------------------------- /packages/site/app/machines/quickstart-button-refined.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET quickstart-button-machine-refined.js 2 | import { Machine } from 'xstate'; 3 | 4 | export default Machine( 5 | { 6 | type: 'parallel', 7 | states: { 8 | interactivity: { 9 | initial: 'unknown', 10 | states: { 11 | unknown: { 12 | on: { 13 | '': [ 14 | { target: 'enabled', cond: 'isEnabled' }, 15 | { target: 'disabled' }, 16 | ], 17 | }, 18 | }, 19 | enabled: { 20 | on: { 21 | DISABLE: 'disabled', 22 | }, 23 | }, 24 | disabled: { 25 | on: { 26 | ENABLE: 'enabled', 27 | }, 28 | }, 29 | }, 30 | }, 31 | activity: { 32 | initial: 'idle', 33 | states: { 34 | idle: { 35 | on: { 36 | SUBMIT: { 37 | target: 'busy', 38 | cond: 'isEnabled', 39 | }, 40 | }, 41 | }, 42 | busy: { 43 | entry: ['handleSubmit'], 44 | on: { 45 | SUCCESS: 'success', 46 | ERROR: 'error', 47 | }, 48 | }, 49 | success: { 50 | entry: ['handleSuccess'], 51 | on: { 52 | SUBMIT: { 53 | target: 'busy', 54 | cond: 'isEnabled', 55 | }, 56 | }, 57 | }, 58 | error: { 59 | entry: ['handleError'], 60 | on: { 61 | SUBMIT: { 62 | target: 'busy', 63 | cond: 'isEnabled', 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | }, 71 | { 72 | actions: { 73 | handleSubmit() {}, 74 | handleSuccess() {}, 75 | handleError() {}, 76 | }, 77 | guards: { 78 | isEnabled(context) { 79 | return !context.disabled; 80 | }, 81 | }, 82 | } 83 | ); 84 | // END-SNIPPET 85 | -------------------------------------------------------------------------------- /packages/site/app/components/page-headings.hbs: -------------------------------------------------------------------------------- 1 |
4 | 5 | {{#if page.headings.length}} 6 |
    7 |
  • 8 | 15 |
  • 16 | {{#each page.headings as |heading|}} 17 |
  • 18 | 29 | {{heading.title}} 30 | 31 | 32 | {{#if heading.headings.length}} 33 | 52 | {{/if}} 53 |
  • 54 | {{/each}} 55 |
56 | {{/if}} 57 |
58 |
59 | -------------------------------------------------------------------------------- /packages/site/app/components/docs-header.hbs: -------------------------------------------------------------------------------- 1 | {{!-- template-lint-disable --}} 2 |
3 |
4 | 5 | {{#docs-header/link "index"}} 6 | 7 | {{svg-jar "statecharts" width=48 height=48 class="docs-fill-none"}} 8 | 9 | {{/docs-header/link}} 10 | 11 |
12 | {{#docs-header/link "docs"}} 13 | Documentation 14 | {{/docs-header/link}} 15 | 16 | {{yield (hash 17 | link=(component "docs-header/link") 18 | )}} 19 | 20 |
21 | 22 | {{#docs-header/link on-click=(action (toggle "isShowingVersionSelector" this))}} 23 | 24 | 25 | {{#if (eq currentVersion.key latestVersionName)}} 26 | {{#if currentVersion.tag}} 27 | {{currentVersion.tag}} 28 | {{else}} 29 | Latest 30 | {{/if}} 31 | {{else}} 32 | {{currentVersion.name}} 33 | {{/if}} 34 | 35 | {{svg-jar "caret" height=12 width=12}} 36 | 37 | {{/docs-header/link}} 38 | 39 | {{#if projectHref}} 40 | {{#docs-header/link href=projectHref}} 41 | 42 | {{svg-jar "github" width=24 height=24}} 43 | 44 | {{/docs-header/link}} 45 | {{/if}} 46 | 47 | {{!-- Something to take up space on mobile, so the scrolling nav isn't hugging the edge --}} 48 |
49 |
50 |
51 |
52 | 53 | {{#if isShowingVersionSelector}} 54 | {{docs-header/version-selector on-close=(action (mut isShowingVersionSelector false))}} 55 | {{/if}} 56 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function matomoSiteIdForDeployTarget(deployTarget) { 4 | if (deployTarget === 'production') { 5 | return '3'; 6 | } 7 | 8 | return '2'; 9 | } 10 | 11 | module.exports = function (environment) { 12 | const deployTarget = process.env.DEPLOY_TARGET || 'development'; 13 | 14 | let ENV = { 15 | modulePrefix: 'dummy', 16 | deployTarget, 17 | environment, 18 | rootURL: '/', 19 | locationType: 'auto', 20 | EmberENV: { 21 | FEATURES: { 22 | // Here you can enable experimental features on an ember canary build 23 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 24 | }, 25 | EXTEND_PROTOTYPES: { 26 | // Prevent Ember Data from overriding Date.parse. 27 | Date: false, 28 | }, 29 | }, 30 | 31 | APP: { 32 | // Here you can pass flags/options to your application instance 33 | // when it is created 34 | }, 35 | metricsAdapters: [ 36 | { 37 | name: 'Matomo', 38 | environments: ['development', 'production'], 39 | config: { 40 | scriptUrl: '//cdn.matomo.cloud/effective-ember.matomo.cloud', 41 | trackerUrl: 'https://effective-ember.matomo.cloud', 42 | siteId: matomoSiteIdForDeployTarget(deployTarget), 43 | disableCookies: true, 44 | }, 45 | }, 46 | ], 47 | }; 48 | 49 | if (environment === 'development') { 50 | // ENV.APP.LOG_RESOLVER = true; 51 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 52 | // ENV.APP.LOG_TRANSITIONS = true; 53 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 54 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 55 | } 56 | 57 | if (environment === 'test') { 58 | // Testem prefers this... 59 | ENV.locationType = 'none'; 60 | 61 | // keep test console output quieter 62 | ENV.APP.LOG_ACTIVE_GENERATION = false; 63 | ENV.APP.LOG_VIEW_LOOKUPS = false; 64 | 65 | ENV.APP.rootElement = '#ember-testing'; 66 | ENV.APP.autoboot = false; 67 | } 68 | 69 | if (environment === 'production') { 70 | // here you can enable a production-specific feature 71 | } 72 | 73 | return ENV; 74 | }; 75 | -------------------------------------------------------------------------------- /packages/ember-statecharts/addon/index.ts: -------------------------------------------------------------------------------- 1 | import { useMachine } from './-private/usables'; 2 | 3 | import { matchesState as xstateMatchesState } from 'xstate'; 4 | 5 | import type { StateValue } from 'xstate'; 6 | 7 | /** 8 | * A decorator that can be used to create a getter that matches against an 9 | * {@link Statechart}'s state and will return either `true` or `false` 10 | * 11 | * 12 | * ```js 13 | * import Component from '@glimmer/component'; 14 | * import buttonMachine from '../machines/button'; 15 | * 16 | * export default class Button extends Component { 17 | * @use statechart = useMachine(this, () => { 18 | * return { 19 | * machine: buttonMachine 20 | * } 21 | * }) 22 | * 23 | * @matchesState('disabled') 24 | * isDisabled; // true when statechart is in `disabled` state 25 | * 26 | * @matchesState({ interactivity: 'disabled' }) 27 | * isDisabled; // it is possible to match against nested/parallel states 28 | * } 29 | * ``` 30 | * 31 | * You can match against any XState [StateValue](https://xstate.js.org/api/globals.html#statevalue) 32 | * 33 | * By default `matchesState` expects your {@link Statechart} to be called `statechart`. 34 | * If you named it differently you can use the second param to this decorator: 35 | * 36 | * 37 | * ```js 38 | * import Component from '@glimmer/component'; 39 | * import buttonMachine from '../machines/button'; 40 | * 41 | * export default class Button extends Component { 42 | * @use sc = useMachine(this, () => { 43 | * return { 44 | * machine: buttonMachine 45 | * } 46 | * }) 47 | * 48 | * @matchesState('disabled', 'sc') 49 | * isDisabled; 50 | * } 51 | * ``` 52 | * 53 | */ 54 | /* eslint-disable @typescript-eslint/no-explicit-any */ 55 | function matchesState( 56 | state: StateValue, 57 | statechartPropertyName = 'statechart' 58 | ): any { 59 | return function () { 60 | return { 61 | get(this: any): boolean { 62 | const statechart = this[statechartPropertyName] as 63 | | { state: { value: StateValue } } 64 | | undefined; 65 | 66 | if (statechart) { 67 | return xstateMatchesState(state, statechart.state.value); 68 | } else { 69 | return false; 70 | } 71 | }, 72 | }; 73 | }; 74 | } 75 | 76 | export { useMachine, matchesState }; 77 | -------------------------------------------------------------------------------- /packages/test-app/config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 5 | 6 | module.exports = async function () { 7 | return { 8 | useYarn: true, 9 | scenarios: [ 10 | { 11 | name: 'ember-lts-3.24', 12 | npm: { 13 | devDependencies: { 14 | 'ember-source': '~3.24.3', 15 | }, 16 | }, 17 | }, 18 | { 19 | name: 'ember-release', 20 | npm: { 21 | devDependencies: { 22 | 'ember-source': await getChannelURL('release'), 23 | 'ember-auto-import': '^2.0.0', 24 | webpack: '^5.0.0', 25 | }, 26 | }, 27 | }, 28 | { 29 | name: 'ember-beta', 30 | npm: { 31 | devDependencies: { 32 | 'ember-source': await getChannelURL('beta'), 33 | 'ember-auto-import': '^2.0.0', 34 | webpack: '^5.0.0', 35 | }, 36 | }, 37 | }, 38 | { 39 | name: 'ember-canary', 40 | npm: { 41 | devDependencies: { 42 | 'ember-source': await getChannelURL('canary'), 43 | 'ember-auto-import': '^2.0.0', 44 | webpack: '^5.0.0', 45 | }, 46 | }, 47 | }, 48 | { 49 | name: 'ember-default-with-jquery', 50 | env: { 51 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 52 | 'jquery-integration': true, 53 | }), 54 | }, 55 | npm: { 56 | devDependencies: { 57 | '@ember/jquery': '^1.1.0', 58 | }, 59 | }, 60 | }, 61 | { 62 | name: 'ember-classic', 63 | env: { 64 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 65 | 'application-template-wrapper': true, 66 | 'default-async-observers': false, 67 | 'template-only-glimmer-components': false, 68 | }), 69 | }, 70 | npm: { 71 | devDependencies: { 72 | 'ember-source': '~3.28.0', 73 | }, 74 | ember: { 75 | edition: 'classic', 76 | }, 77 | }, 78 | }, 79 | embroiderSafe(), 80 | embroiderOptimized(), 81 | ], 82 | }; 83 | }; 84 | -------------------------------------------------------------------------------- /packages/site/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function matomoSiteIdForDeployTarget(deployTarget) { 4 | if (deployTarget === 'production') { 5 | return '3'; 6 | } 7 | 8 | return '2'; 9 | } 10 | 11 | module.exports = function (environment) { 12 | const deployTarget = process.env.DEPLOY_TARGET || 'development'; 13 | 14 | let ENV = { 15 | modulePrefix: 'site', 16 | deployTarget, 17 | environment, 18 | rootURL: '/', 19 | locationType: 'auto', 20 | EmberENV: { 21 | FEATURES: { 22 | // Here you can enable experimental features on an ember canary build 23 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true 24 | }, 25 | EXTEND_PROTOTYPES: { 26 | // Prevent Ember Data from overriding Date.parse. 27 | Date: false, 28 | }, 29 | }, 30 | 31 | APP: { 32 | // Here you can pass flags/options to your application instance 33 | // when it is created 34 | }, 35 | metricsAdapters: [ 36 | { 37 | name: 'Matomo', 38 | environments: ['development', 'production'], 39 | config: { 40 | scriptUrl: '//cdn.matomo.cloud/effective-ember.matomo.cloud', 41 | trackerUrl: 'https://effective-ember.matomo.cloud', 42 | siteId: matomoSiteIdForDeployTarget(deployTarget), 43 | disableCookies: true, 44 | }, 45 | }, 46 | ], 47 | }; 48 | 49 | if (process.env.DEPLOY_TARGET) { 50 | const versionedDocInfo = require('../lib/versioned-doc-info'); 51 | ENV.rootURL = versionedDocInfo(process.env.DEPLOY_TARGET).rootURL; 52 | } 53 | 54 | if (environment === 'development') { 55 | // ENV.APP.LOG_RESOLVER = true; 56 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 57 | // ENV.APP.LOG_TRANSITIONS = true; 58 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 59 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 60 | } 61 | 62 | if (environment === 'test') { 63 | // Testem prefers this... 64 | ENV.locationType = 'none'; 65 | 66 | // keep test console output quieter 67 | ENV.APP.LOG_ACTIVE_GENERATION = false; 68 | ENV.APP.LOG_VIEW_LOOKUPS = false; 69 | 70 | ENV.APP.rootElement = '#ember-testing'; 71 | ENV.APP.autoboot = false; 72 | } 73 | 74 | if (environment === 'production') { 75 | // here you can enable a production-specific feature 76 | } 77 | 78 | return ENV; 79 | }; 80 | -------------------------------------------------------------------------------- /packages/site/app/components/quickstart-button-refined.js: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET quickstart-button-final 2 | import Component from '@glimmer/component'; 3 | import { action } from '@ember/object'; 4 | import { task } from 'ember-concurrency'; 5 | import { matchesState } from 'ember-statecharts'; 6 | import { useMachine } from 'ember-statecharts/-private/usables'; 7 | import quickstartButtonRefinedMachine from '../machines/quickstart-button-refined'; 8 | 9 | function noop() {} 10 | 11 | export default class QuickstartButtonFinal extends Component { 12 | get onClick() { 13 | return this.args.onClick || noop; 14 | } 15 | 16 | @matchesState({ activity: 'busy' }) 17 | isBusy; 18 | 19 | @matchesState({ interactivity: 'disabled' }) 20 | isDisabled; 21 | 22 | @matchesState({ interactivity: 'unknown' }) 23 | isInteractivityUnknown; 24 | 25 | get showAsDisabled() { 26 | const { isDisabled, isBusy, isInteractivityUnknown } = this; 27 | 28 | return isDisabled || isBusy || isInteractivityUnknown; 29 | } 30 | 31 | statechart = useMachine(this, () => { 32 | return { 33 | machine: quickstartButtonRefinedMachine 34 | .withContext({ 35 | disabled: this.args.disabled, 36 | }) 37 | .withConfig({ 38 | actions: { 39 | handleSubmit: this.performSubmitTask, 40 | handleSuccess: this.onSuccess, 41 | handleError: this.onError, 42 | }, 43 | guards: { 44 | isEnabled({ disabled }) { 45 | return !disabled; 46 | }, 47 | }, 48 | }), 49 | update: ({ send, machine: { context } }) => { 50 | const { disabled } = context; 51 | 52 | if (disabled) { 53 | send('DISABLE'); 54 | } else { 55 | send('ENABLE'); 56 | } 57 | }, 58 | }; 59 | }); 60 | 61 | @task(function* () { 62 | try { 63 | const result = yield this.onClick(); 64 | this.statechart.send('SUCCESS', { result }); 65 | } catch (e) { 66 | this.statechart.send('ERROR', { error: e }); 67 | } 68 | }) 69 | handleSubmitTask; 70 | 71 | @action 72 | handleClick() { 73 | this.statechart.send('SUBMIT'); 74 | } 75 | 76 | @action 77 | onSuccess(_context, { result }) { 78 | return this.args.onSuccess(result) || noop(); 79 | } 80 | 81 | @action 82 | onError(_context, { error }) { 83 | return this.args.onError(error) || noop(); 84 | } 85 | 86 | @action 87 | performSubmitTask() { 88 | this.handleSubmitTask.perform(); 89 | } 90 | } 91 | // END-SNIPPET 92 | -------------------------------------------------------------------------------- /packages/site/lib/deploy-versioned-doc/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | const versionedDocInfo = require('../versioned-doc-info'); 5 | const path = require('path'); 6 | const fs = require('fs-extra'); 7 | const os = require('os'); 8 | 9 | module.exports = { 10 | name: require('./package').name, 11 | 12 | createDeployPlugin: function (options) { 13 | return { 14 | name: options.name, 15 | 16 | async willUpload(context) { 17 | const { 18 | deployTarget, 19 | distDir, 20 | gitDeploy: { worktreePath }, 21 | } = context; 22 | 23 | const { name, destDir, rootURL, sha, tag } = versionedDocInfo( 24 | deployTarget 25 | ); 26 | 27 | // cache current build 28 | let currentBuild = path.join(os.tmpdir(), 'current-build-'); 29 | fs.moveSync(distDir, currentBuild); 30 | 31 | // ensure we have a ${worktree}/versions/ 32 | let versions = path.resolve(worktreePath, 'versions'); 33 | fs.ensureDirSync(versions); 34 | 35 | if (deployTarget === 'latest') { 36 | // keep the current versions 37 | fs.copySync(versions, path.resolve(currentBuild, 'versions')); 38 | 39 | // make current build the new dist 40 | fs.moveSync(currentBuild, distDir); 41 | } else { 42 | // keep current state of gh-pages 43 | fs.copySync(worktreePath, distDir); 44 | 45 | // move current build into versions/${branch || tag} 46 | fs.moveSync(currentBuild, path.resolve(distDir, destDir), { 47 | overwrite: true, 48 | }); 49 | } 50 | 51 | // get ${worktree}/versions.json 52 | let versionsJSON = {}; 53 | let versionsFile = path.resolve(worktreePath, 'versions.json'); 54 | if (fs.existsSync(versionsFile)) { 55 | versionsJSON = fs.readJsonSync(versionsFile); 56 | } 57 | 58 | let key = name; 59 | 60 | // update key for latest, this is for ember-cli-addon-docs compatibility 61 | if (deployTarget === 'latest') { 62 | key = '-latest'; 63 | } 64 | 65 | versionsJSON[key] = { 66 | path: rootURL, 67 | name, 68 | sha, 69 | tag, 70 | }; 71 | 72 | // write current versions.json 73 | fs.writeFileSync( 74 | path.resolve(distDir, 'versions.json'), 75 | JSON.stringify(versionsJSON, null, 2) 76 | ); 77 | 78 | fs.removeSync(currentBuild); 79 | }, 80 | }; 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /packages/site/app/templates/docs.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 | 9 |
10 | 11 | 12 |
15 | {{outlet}} 16 |
17 |
18 |
21 | 22 |
23 | {{#if previous}} 24 | 29 | 34 | 35 | 36 | 37 | {{previous.title}} 38 | 39 | {{/if}} 40 |
41 |
42 | {{#if next}} 43 | 44 | {{next.title}} 45 | 46 | 47 | 52 | 57 | 58 | {{/if}} 59 |
60 |
61 |
62 |
63 | 66 |
67 |
68 | -------------------------------------------------------------------------------- /packages/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": "ember build --environment=production", 15 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", 16 | "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", 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 | "start": "ember serve", 22 | "test": "npm-run-all lint test:*", 23 | "test:ember": "ember test --test-port=0", 24 | "test:ember-compatibility": "ember try:each" 25 | }, 26 | "devDependencies": { 27 | "@ember/optional-features": "^2.0.0", 28 | "@ember/test-helpers": "^2.4.2", 29 | "@embroider/test-setup": "^0.43.5", 30 | "@glimmer/component": "^1.0.4", 31 | "@glimmer/tracking": "^1.0.4", 32 | "babel-eslint": "^10.1.0", 33 | "broccoli-asset-rev": "^3.0.0", 34 | "ember-auto-import": "^1.11.3", 35 | "ember-cli": "~3.28.4", 36 | "ember-cli-app-version": "^5.0.0", 37 | "ember-cli-babel": "^7.26.6", 38 | "ember-cli-dependency-checker": "^3.2.0", 39 | "ember-cli-htmlbars": "^5.7.1", 40 | "ember-cli-inject-live-reload": "^2.1.0", 41 | "ember-cli-sri": "^2.1.1", 42 | "ember-cli-terser": "^4.0.2", 43 | "ember-data": "~3.28.0", 44 | "ember-export-application-global": "^2.0.1", 45 | "ember-fetch": "^8.1.1", 46 | "ember-load-initializers": "^2.1.2", 47 | "ember-maybe-import-regenerator": "^0.1.6", 48 | "ember-page-title": "^6.2.2", 49 | "ember-qunit": "^5.1.4", 50 | "ember-resolver": "^8.0.2", 51 | "ember-source": "~3.28.0", 52 | "ember-source-channel-url": "^3.0.0", 53 | "ember-statecharts": "^0.13.2", 54 | "ember-template-lint": "^3.6.0", 55 | "ember-try": "^1.4.0", 56 | "eslint": "^7.32.0", 57 | "eslint-config-prettier": "^8.3.0", 58 | "eslint-plugin-ember": "^10.5.4", 59 | "eslint-plugin-node": "^11.1.0", 60 | "eslint-plugin-prettier": "^3.4.1", 61 | "eslint-plugin-qunit": "^6.2.0", 62 | "loader.js": "^4.7.0", 63 | "npm-run-all": "^4.1.5", 64 | "prettier": "^2.3.2", 65 | "qunit": "^2.16.0", 66 | "qunit-dom": "^1.6.0", 67 | "xstate": "^4.12.0" 68 | }, 69 | "engines": { 70 | "node": "12.* || 14.* || >= 16" 71 | }, 72 | "ember": { 73 | "edition": "octane" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/site/app/machines/typed-button.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | // BEGIN-SNIPPET typed-button-machine 3 | import { createMachine } from 'xstate'; 4 | 5 | export interface ButtonContext { 6 | disabled?: boolean; 7 | } 8 | 9 | export type ButtonEvent = 10 | | { type: 'SUBMIT' } 11 | | { type: 'SUCCESS'; result: any } 12 | | { type: 'ERROR'; error: any } 13 | | { type: 'ENABLE' } 14 | | { type: 'DISABLE' }; 15 | 16 | export type ButtonState = 17 | | { value: 'idle'; context: { disabled?: boolean } } 18 | | { value: 'busy'; context: { disabled?: boolean } } 19 | | { value: 'success'; context: { disabled?: boolean } } 20 | | { value: 'error'; context: { disabled?: boolean } }; 21 | 22 | export default createMachine( 23 | { 24 | type: 'parallel', 25 | states: { 26 | interactivity: { 27 | initial: 'unknown', 28 | states: { 29 | unknown: { 30 | on: { 31 | '': [ 32 | { target: 'enabled', cond: 'isEnabled' }, 33 | { target: 'disabled' }, 34 | ], 35 | }, 36 | }, 37 | enabled: { 38 | on: { 39 | DISABLE: 'disabled', 40 | }, 41 | }, 42 | disabled: { 43 | on: { 44 | ENABLE: 'enabled', 45 | }, 46 | }, 47 | }, 48 | }, 49 | activity: { 50 | initial: 'idle', 51 | states: { 52 | idle: { 53 | on: { 54 | SUBMIT: { 55 | target: 'busy', 56 | cond: 'isEnabled', 57 | }, 58 | }, 59 | }, 60 | busy: { 61 | entry: ['handleSubmit'], 62 | on: { 63 | SUCCESS: 'success', 64 | ERROR: 'error', 65 | }, 66 | }, 67 | success: { 68 | entry: ['handleSuccess'], 69 | on: { 70 | SUBMIT: { 71 | target: 'busy', 72 | cond: 'isEnabled', 73 | }, 74 | }, 75 | }, 76 | error: { 77 | entry: ['handleError'], 78 | on: { 79 | SUBMIT: { 80 | target: 'busy', 81 | cond: 'isEnabled', 82 | }, 83 | }, 84 | }, 85 | }, 86 | }, 87 | }, 88 | }, 89 | { 90 | actions: { 91 | handleSubmit() {}, 92 | handleSuccess() {}, 93 | handleError() {}, 94 | }, 95 | guards: { 96 | isEnabled(context) { 97 | return !context.disabled; 98 | }, 99 | }, 100 | } 101 | ); 102 | // END-SNIPPET 103 | -------------------------------------------------------------------------------- /packages/site/public/ember-statecharts-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Ember 33 | Statecharts 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /packages/site/app/components/typed-button.ts: -------------------------------------------------------------------------------- 1 | // BEGIN-SNIPPET typed-button 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | import Component from '@glimmer/component'; 4 | import { useMachine, matchesState } from 'ember-statecharts'; 5 | import buttonMachine, { 6 | ButtonContext, 7 | ButtonEvent, 8 | ButtonState, 9 | } from '../machines/typed-button'; 10 | import { TaskGenerator } from 'ember-concurrency'; 11 | 12 | import { task } from 'ember-concurrency-decorators'; 13 | import { taskFor } from 'ember-concurrency-ts'; 14 | 15 | import { action } from '@ember/object'; 16 | 17 | interface ButtonArgs { 18 | disabled?: boolean; 19 | onClick?: () => any; 20 | onSuccess?: (result: any) => any; 21 | onError?: (error: any) => any; 22 | } 23 | 24 | /* eslint-disable-next-line @typescript-eslint/no-empty-function */ 25 | function noop() {} 26 | 27 | export default class TypedButton extends Component { 28 | get onClick(): any { 29 | return this.args.onClick || noop; 30 | } 31 | 32 | @matchesState({ activity: 'busy' }) 33 | isBusy!: boolean; 34 | 35 | @matchesState({ interactivity: 'disabled' }) 36 | isDisabled!: boolean; 37 | 38 | @matchesState({ interactivity: 'unknown' }) 39 | isInteractivityUnknown!: boolean; 40 | 41 | get showAsDisabled(): boolean { 42 | const { isDisabled, isBusy, isInteractivityUnknown } = this; 43 | 44 | return isDisabled || isBusy || isInteractivityUnknown; 45 | } 46 | 47 | statechart = useMachine( 48 | this, 49 | () => { 50 | return { 51 | machine: buttonMachine 52 | .withContext({ 53 | disabled: this.args.disabled, 54 | }) 55 | .withConfig({ 56 | actions: { 57 | handleSubmit: this.performSubmitTask, 58 | handleSuccess: this.onSuccess, 59 | handleError: this.onError, 60 | }, 61 | }), 62 | update: ({ machine, send }) => { 63 | const disabled = machine.context?.disabled; 64 | 65 | if (disabled) { 66 | send('DISABLE'); 67 | } else { 68 | send('ENABLE'); 69 | } 70 | }, 71 | }; 72 | } 73 | ); 74 | 75 | @task *submitTask(): TaskGenerator { 76 | try { 77 | const result = yield this.onClick(); 78 | 79 | this.statechart.send('SUCCESS', { result }); 80 | } catch (e) { 81 | this.statechart.send('ERROR', { error: e }); 82 | } 83 | } 84 | 85 | @action 86 | handleClick(): void { 87 | this.statechart.send('SUBMIT'); 88 | } 89 | 90 | @action 91 | onSuccess( 92 | _context: ButtonContext, 93 | { result }: Extract 94 | ): any { 95 | const functionToCall = this.args.onSuccess || noop; 96 | 97 | return functionToCall(result); 98 | } 99 | 100 | @action 101 | onError( 102 | _context: ButtonContext, 103 | { error }: Extract 104 | ): any { 105 | const functionToCall = this.args.onError || noop; 106 | 107 | return functionToCall(error); 108 | } 109 | 110 | @action 111 | performSubmitTask(): void { 112 | taskFor(this.submitTask).perform(); 113 | } 114 | } 115 | // END-SNIPPET 116 | -------------------------------------------------------------------------------- /packages/site/public/ember-statecharts-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Ember 33 | Statecharts 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow is largely based on 2 | # https://github.com/salsify/ember-css-modules/blob/master/.github/workflows/ci.yml 3 | 4 | name: CI 5 | 6 | # These trigger patterns courtesy of https://github.com/broccolijs/broccoli/pull/436 7 | on: 8 | pull_request: 9 | push: 10 | # filtering branches here prevents duplicate builds from pull_request and push 11 | branches: 12 | - master 13 | - 'v*' 14 | # always run CI for tags 15 | tags: 16 | - '*' 17 | 18 | # early issue detection: run CI weekly on Sundays 19 | schedule: 20 | - cron: '0 6 * * 0' 21 | 22 | env: 23 | CI: true 24 | 25 | jobs: 26 | test: 27 | name: Test 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout Code 31 | uses: actions/checkout@v2 32 | - name: Install Node 33 | uses: actions/setup-node@v2 34 | with: 35 | node-version: 14 36 | - name: Install Dependencies 37 | run: yarn install --frozen-lockfile 38 | - name: Lerna Bootstrap 39 | run: yarn run lerna bootstrap 40 | - name: Lint 41 | run: yarn run lerna run lint 42 | - name: Tests 43 | run: yarn run lerna run test:ember 44 | 45 | test-locked-deps: 46 | name: Locked Deps 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Checkout Code 50 | uses: actions/checkout@v2 51 | - name: Install Node 52 | uses: actions/setup-node@v2 53 | with: 54 | node-version: 14 55 | - name: Install Dependencies 56 | run: yarn install --frozen-lockfile 57 | - name: Lerna Bootstrap 58 | run: yarn run lerna bootstrap 59 | - name: Lint 60 | run: yarn run lerna run lint 61 | - name: Browser Tests 62 | run: yarn run lerna run test:ember 63 | 64 | test-old-dependencies: 65 | name: Oldest Supported Env 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Checkout Code 69 | uses: actions/checkout@v2 70 | - name: Install Node 71 | uses: actions/setup-node@v2 72 | with: 73 | node-version: 12 74 | - name: Install Dependencies 75 | run: yarn install --frozen-lockfile 76 | - name: Lerna Bootstrap 77 | run: yarn run lerna bootstrap 78 | - name: Browser Tests 79 | run: yarn run lerna run test:ember 80 | 81 | test-try: 82 | name: Ember Try (${{ matrix.scenario }}) 83 | runs-on: ubuntu-latest 84 | strategy: 85 | fail-fast: false 86 | matrix: 87 | include: 88 | - scenario: ember-lts-3.24 89 | allow-failure: false 90 | - scenario: ember-release 91 | allow-failure: false 92 | - scenario: ember-beta 93 | allow-failure: false 94 | - scenario: ember-canary 95 | allow-failure: false 96 | steps: 97 | - name: Checkout Code 98 | uses: actions/checkout@v2 99 | - name: Install Node 100 | uses: actions/setup-node@v2 101 | with: 102 | node-version: 14 103 | - name: Install Dependencies 104 | run: yarn install --frozen-lockfile 105 | - name: Lerna Bootstrap 106 | run: yarn run lerna bootstrap 107 | - name: Try Scenario 108 | run: yarn run lerna exec --stream --scope test-app ember try:one ${{ matrix.scenario }} 109 | continue-on-error: ${{ matrix.allow-failure }} 110 | -------------------------------------------------------------------------------- /packages/ember-statecharts/addon/-private/resource.ts: -------------------------------------------------------------------------------- 1 | import { setHelperManager, capabilities } from '@ember/helper'; 2 | 3 | import { setOwner } from '@ember/application'; 4 | import { 5 | destroy, 6 | associateDestroyableChild, 7 | registerDestructor, 8 | } from '@ember/destroyable'; 9 | 10 | import { createCache, getValue } from '@glimmer/tracking/primitives/cache'; 11 | 12 | import type { Cache } from '@glimmer/validator'; 13 | 14 | type Owner = Record; 15 | 16 | export interface TemplateArgs { 17 | positional: readonly unknown[]; 18 | named: Record; 19 | } 20 | 21 | declare const HELPER_DEFINITION: unique symbol; 22 | type HelperDefinition = T & { 23 | readonly [HELPER_DEFINITION]: true; 24 | }; 25 | 26 | export abstract class Resource { 27 | protected readonly args!: Args; 28 | 29 | constructor(owner: Owner, args: Args) { 30 | setOwner(this, owner); 31 | 32 | this.args = args; 33 | } 34 | 35 | // eslint-disable-next-line @typescript-eslint/no-empty-function 36 | setup(): void {} 37 | 38 | update?(args: Args): void; 39 | teardown?(): void; 40 | } 41 | 42 | class ResourceManager { 43 | readonly capabilities = capabilities('3.23', { 44 | hasValue: true, 45 | hasDestroyable: true, 46 | }); 47 | 48 | private readonly owner: Owner; 49 | 50 | constructor(owner: Owner) { 51 | this.owner = owner; 52 | } 53 | 54 | createHelper( 55 | Class: HelperDefinition Resource>, 56 | args: Args 57 | ): Cache { 58 | const { update, teardown } = Class.prototype as Resource; 59 | 60 | const hasUpdate = typeof update === 'function'; 61 | const hasTeardown = typeof teardown === 'function'; 62 | 63 | const owner = this.owner; 64 | 65 | let instance: Resource | undefined; 66 | let cache: Cache; 67 | 68 | if (hasUpdate) { 69 | cache = createCache(() => { 70 | if (instance === undefined) { 71 | instance = setupInstance(cache, Class, owner, args, hasTeardown); 72 | } else { 73 | instance.update!(args); // eslint-disable-line 74 | } 75 | 76 | return instance; 77 | }); 78 | } else { 79 | cache = createCache(() => { 80 | if (instance !== undefined) { 81 | destroy(instance); 82 | } 83 | 84 | instance = setupInstance(cache, Class, owner, args, hasTeardown); 85 | 86 | return instance; 87 | }); 88 | } 89 | 90 | return cache; 91 | } 92 | 93 | getValue(cache: Cache): Resource | undefined { 94 | const instance = getValue(cache); 95 | 96 | return instance; 97 | } 98 | 99 | getDestroyable(cache: Cache): Cache { 100 | return cache; 101 | } 102 | 103 | //eslint-disable-next-line 104 | getDebugName(fn: (...args: any[]) => void): string { 105 | return fn.name || '(anonymous function)'; 106 | } 107 | } 108 | 109 | function setupInstance( 110 | cache: Cache, 111 | Class: new (owner: Owner, args: TemplateArgs) => T, 112 | owner: Owner, 113 | args: TemplateArgs, 114 | hasTeardown: boolean 115 | ): T { 116 | const instance = new Class(owner, args); 117 | associateDestroyableChild(cache, instance); 118 | instance.setup(); 119 | 120 | if (hasTeardown) { 121 | registerDestructor(instance, () => instance.teardown!()); // eslint-disable-line 122 | } 123 | 124 | return instance; 125 | } 126 | 127 | setHelperManager((owner: Owner) => new ResourceManager(owner), Resource); 128 | -------------------------------------------------------------------------------- /packages/site/app/modifiers/intersect-headings.js: -------------------------------------------------------------------------------- 1 | import Modifier from 'ember-modifier'; 2 | import { action } from '@ember/object'; 3 | 4 | function getHeadingIds(headings, output = []) { 5 | if (typeof headings === 'undefined') { 6 | return []; 7 | } 8 | headings.forEach((heading) => { 9 | output.push(heading.id); 10 | getHeadingIds(heading.headings, output); 11 | }); 12 | return output; 13 | } 14 | 15 | export default class IntersectHeadingsModifier extends Modifier { 16 | handler = null; 17 | headings = []; 18 | observer = null; 19 | activeIndex = null; 20 | 21 | @action 22 | handleObserver(elements) { 23 | // Based on https://taylor.callsen.me/modern-navigation-menus-with-css-position-sticky-and-intersectionobservers/ 24 | 25 | // current index must be memoized or tracked outside of function for comparison 26 | let localActiveIndex = this.activeIndex; 27 | 28 | // track which elements register above or below the document's current position 29 | const aboveIndeces = []; 30 | const belowIndeces = []; 31 | 32 | // loop through each intersection element 33 | // due to the asychronous nature of observers, callbacks must be designed to handle 1 or many intersecting elements 34 | elements.forEach((element) => { 35 | // detect if intersecting element is above the browser viewport; include cross browser logic 36 | const boundingClientRectY = 37 | typeof element.boundingClientRect.y !== 'undefined' 38 | ? element.boundingClientRect.y 39 | : element.boundingClientRect.top; 40 | const rootBoundsY = 41 | typeof element.rootBounds.y !== 'undefined' 42 | ? element.rootBounds.y 43 | : element.rootBounds.top; 44 | const isAbove = boundingClientRectY < rootBoundsY; 45 | 46 | const id = element.target.getAttribute('id'); 47 | const intersectingElemIdx = this.headings.findIndex((item) => item == id); 48 | 49 | // record index as either above or below current index 50 | if (isAbove) aboveIndeces.push(intersectingElemIdx); 51 | else belowIndeces.push(intersectingElemIdx); 52 | }); 53 | 54 | // determine min and max fired indeces values (support for multiple elements firing at once) 55 | const minIndex = Math.min(...belowIndeces); 56 | const maxIndex = Math.max(...aboveIndeces); 57 | 58 | // determine how to adjust localActiveIndex based on scroll direction 59 | if (aboveIndeces.length > 0) { 60 | // scrolling down - set to max of fired indeces 61 | localActiveIndex = maxIndex; 62 | } else if (belowIndeces.length > 0 && minIndex <= this.activeIndex) { 63 | // scrolling up - set to minimum of fired indeces 64 | localActiveIndex = minIndex - 1 >= 0 ? minIndex - 1 : 0; 65 | } 66 | 67 | // render new index to DOM (if required) 68 | if (localActiveIndex != this.activeIndex) { 69 | this.activeIndex = localActiveIndex; 70 | 71 | this.handler(this.headings[this.activeIndex]); 72 | } 73 | } 74 | 75 | observe() { 76 | if ('IntersectionObserver' in window) { 77 | this.observer = new IntersectionObserver(this.handleObserver, { 78 | rootMargin: '-96px', // Distance from top to heading id 79 | threshold: 1.0, 80 | }); 81 | 82 | this.headings.forEach((id) => { 83 | const el = document.getElementById(id); 84 | if (el) { 85 | this.observer.observe(el); 86 | } 87 | }); 88 | } 89 | } 90 | 91 | unobserve() { 92 | if (this.observer) { 93 | this.observer.disconnect(); 94 | } 95 | } 96 | 97 | didUpdateArguments() { 98 | this.unobserve(); 99 | } 100 | 101 | didReceiveArguments() { 102 | const [handler] = this.args.positional; 103 | this.handler = handler; 104 | this.headings = getHeadingIds(this.args.named.headings); 105 | 106 | this.observe(); 107 | } 108 | 109 | willRemove() { 110 | this.unobserve(); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /packages/site/tests/integration/components/x-button-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, click, waitFor } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | import { Promise, resolve, reject } from 'rsvp'; 6 | 7 | module('Integration | Component | quickstart-button', function (hooks) { 8 | setupRenderingTest(hooks); 9 | 10 | test('it renders a button', async function (assert) { 11 | await render(hbs``); 12 | 13 | assert.dom('button').exists('button is rendered'); 14 | }); 15 | 16 | test('it is possible to use the button in block-format', async function (assert) { 17 | let TEXT = 'Hello'; 18 | 19 | this.set('text', TEXT); 20 | 21 | await render(hbs` 22 | 23 | {{this.text}} World! 24 | 25 | `); 26 | 27 | assert.dom('[data-test-button]').hasText('Hello World!'); 28 | }); 29 | 30 | test("it's possible to pass an onClick-action handler to the button", async function (assert) { 31 | assert.expect(1); 32 | 33 | this.set('wat', function () { 34 | assert.ok(true, 'action handler was called'); 35 | }); 36 | 37 | await render(hbs` 38 | 42 | `); 43 | 44 | await click('[data-test-button]'); 45 | }); 46 | 47 | test("when the action triggered by the button clicked gets fired and takes time it's not possible to trigger it again", async function (assert) { 48 | assert.expect(3); 49 | 50 | this.set('onClick', function () { 51 | assert.ok(true, 'onClick was triggered'); 52 | return new Promise(function () {}); 53 | }); 54 | 55 | await render(hbs` 56 | 57 | `); 58 | 59 | click('[data-test-button]'); 60 | 61 | await waitFor('[data-test-loading]'); 62 | 63 | assert 64 | .dom('[data-test-loading]') 65 | .exists('button is displayed with loading ui while busy'); 66 | 67 | this.set('onClick', function () { 68 | assert.ok(false, 'onClick should not be triggered again'); 69 | }); 70 | 71 | assert.dom('[data-test-button]').isDisabled('button is disabled when busy'); 72 | }); 73 | 74 | test("when passing the disabled property the button is disabled and won't trigger its action", async function (assert) { 75 | assert.expect(1); 76 | 77 | this.set('onClick', function () { 78 | assert.ok(false, 'onClick should not be triggered'); 79 | }); 80 | 81 | await render(hbs` 82 | 88 | `); 89 | 90 | assert.dom('[data-test-button]').isDisabled('button is disabled'); 91 | }); 92 | 93 | test('when the triggered action resolves the `onSuccess` handler is triggered', async function (assert) { 94 | assert.expect(1); 95 | 96 | this.set('onClick', function () { 97 | return resolve(); 98 | }); 99 | 100 | this.set('onSuccess', function () { 101 | assert.ok(true, '`onSuccess` was triggered'); 102 | }); 103 | 104 | await render(hbs` 105 | 111 | `); 112 | 113 | await click('[data-test-button]'); 114 | }); 115 | 116 | test('when the triggered action rejects the `onError` handler is triggered', async function (assert) { 117 | assert.expect(1); 118 | 119 | this.set('onClick', function () { 120 | return reject(); 121 | }); 122 | 123 | this.set('onError', function () { 124 | assert.ok(true, '`onError` was triggered'); 125 | }); 126 | 127 | await render(hbs` 128 | 134 | `); 135 | 136 | await click('[data-test-button]'); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /packages/site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "site", 4 | "version": "0.13.2", 5 | "description": "Site for ember-statecharts", 6 | "homepage": "https://ember-statecharts.com/", 7 | "repository": "https://github.com/LevelbossMike/ember-statecharts", 8 | "license": "MIT", 9 | "author": "", 10 | "types": "./index.d.ts", 11 | "scripts": { 12 | "build": "ember build --environment=production", 13 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel 'lint:!(fix)'", 14 | "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", 15 | "lint:hbs": "ember-template-lint .", 16 | "lint:hbs:fix": "ember-template-lint . --fix", 17 | "lint:js": "eslint . --cache", 18 | "lint:js:fix": "eslint . --fix", 19 | "prepublishOnly": "ember ts:precompile", 20 | "postpublish": "ember ts:clean", 21 | "start": "ember serve", 22 | "test": "npm-run-all lint test:*", 23 | "test:ember": "ember test --test-port=0" 24 | }, 25 | "dependencies": { 26 | "@glimmer/tracking": "^1.0.3", 27 | "ember-auto-import": "^1.10.1", 28 | "ember-cli-babel": "^7.23.0", 29 | "ember-cli-htmlbars": "^5.3.1", 30 | "ember-cli-typescript": "^3.1.4", 31 | "ember-statecharts": "0.13.2" 32 | }, 33 | "devDependencies": { 34 | "@docfy/ember": "^0.4.3", 35 | "@docfy/plugin-with-prose": "^0.4.2", 36 | "@ember/optional-features": "^2.0.0", 37 | "@ember/render-modifiers": "^1.0.2", 38 | "@ember/test-helpers": "^2.1.4", 39 | "@glimmer/component": "^1.0.3", 40 | "@mapbox/rehype-prism": "^0.6.0", 41 | "@release-it/conventional-changelog": "^1.1.4", 42 | "@tailwindcss/aspect-ratio": "^0.2.0", 43 | "@tailwindcss/typography": "^0.4.0", 44 | "@types/ember": "^3.16.5", 45 | "@types/ember-qunit": "^3.4.9", 46 | "@types/ember__test-helpers": "^1.7.2", 47 | "@types/qunit": "^2.9.1", 48 | "@types/rsvp": "^4.0.3", 49 | "@typescript-eslint/eslint-plugin": "^4.0.1", 50 | "@typescript-eslint/parser": "^4.0.1", 51 | "autoprefixer": "^10.2.5", 52 | "babel-eslint": "^10.1.0", 53 | "broccoli-asset-rev": "^3.0.0", 54 | "ember-angle-bracket-invocation-polyfill": "^2.0.2", 55 | "ember-cli": "~3.24.0", 56 | "ember-cli-dependency-checker": "^3.2.0", 57 | "ember-cli-deploy": "^1.0.2", 58 | "ember-cli-deploy-build": "^2.0.0", 59 | "ember-cli-deploy-git": "^1.3.4", 60 | "ember-cli-fastboot": "^2.2.3", 61 | "ember-cli-inject-live-reload": "^2.0.2", 62 | "ember-cli-postcss": "^7.0.0", 63 | "ember-cli-sri": "^2.1.1", 64 | "ember-cli-terser": "^4.0.1", 65 | "ember-cli-typescript-blueprints": "^3.0.0", 66 | "ember-concurrency": "^1.2.1", 67 | "ember-concurrency-async": "^0.3.1", 68 | "ember-concurrency-decorators": "^2.0.1", 69 | "ember-concurrency-ts": "^0.2.0", 70 | "ember-disable-prototype-extensions": "^1.1.3", 71 | "ember-export-application-global": "^2.0.1", 72 | "ember-load-initializers": "^2.1.2", 73 | "ember-maybe-import-regenerator": "^0.1.6", 74 | "ember-metrics": "^0.16.0", 75 | "ember-metrics-matomo-adapter": "^0.1.0", 76 | "ember-modifier": "^2.1.1", 77 | "ember-page-title": "^6.2.1", 78 | "ember-qunit": "^5.1.1", 79 | "ember-qunit-assert-helpers": "^0.2.2", 80 | "ember-resolver": "^8.0.2", 81 | "ember-showcase": "^0.2.0", 82 | "ember-source": "~3.24.0", 83 | "ember-source-channel-url": "^3.0.0", 84 | "ember-svg-jar": "^2.3.3", 85 | "ember-template-lint": "^2.15.0", 86 | "ember-try": "^1.4.0", 87 | "eslint": "^7.17.0", 88 | "eslint-config-prettier": "^7.1.0", 89 | "eslint-plugin-ember": "^10.1.1", 90 | "eslint-plugin-node": "^11.1.0", 91 | "eslint-plugin-prettier": "^3.3.1", 92 | "fs-extra": "^9.1.0", 93 | "git-repo-info": "^2.1.1", 94 | "loader.js": "^4.7.0", 95 | "npm-run-all": "^4.1.5", 96 | "postcss-import": "^14.0.2", 97 | "prember": "^1.0.5", 98 | "prember-crawler": "^1.0.0", 99 | "prettier": "^2.2.1", 100 | "qunit": "^2.13.0", 101 | "qunit-dom": "^1.6.0", 102 | "release-it": "^13.6.2", 103 | "remark-autolink-headings": "^6.0.1", 104 | "tailwindcss": "^2.1.2", 105 | "typedoc": "^0.20.36", 106 | "typescript": "^4.0.2", 107 | "xstate": "^4.12.0" 108 | }, 109 | "engines": { 110 | "node": "10.* || >= 12" 111 | }, 112 | "ember": { 113 | "edition": "octane" 114 | }, 115 | "ember-addon": { 116 | "paths": [ 117 | "lib/deploy-versioned-doc" 118 | ] 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /packages/ember-statecharts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-statecharts", 3 | "version": "0.13.2", 4 | "description": "Statecharts for Ember.js applications", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "homepage": "https://ember-statecharts.com/", 9 | "repository": "https://github.com/LevelbossMike/ember-statecharts", 10 | "license": "MIT", 11 | "author": "", 12 | "types": "./index.d.ts", 13 | "directories": { 14 | "doc": "doc", 15 | "test": "tests" 16 | }, 17 | "scripts": { 18 | "api-docs": "typedoc addon --out ../site/public/api-docs", 19 | "build": "ember build --environment=production", 20 | "changelog": "lerna-changelog", 21 | "lint": "npm-run-all --aggregate-output --continue-on-error --parallel \"lint:!(fix)\"", 22 | "lint:fix": "npm-run-all --aggregate-output --continue-on-error --parallel lint:*:fix", 23 | "lint:hbs": "ember-template-lint .", 24 | "lint:hbs:fix": "ember-template-lint . --fix", 25 | "lint:js": "eslint . --cache", 26 | "lint:js:fix": "eslint . --fix", 27 | "prepublishOnly": "ember ts:precompile", 28 | "postpublish": "ember ts:clean", 29 | "start": "ember serve", 30 | "test": "npm-run-all lint test:*", 31 | "test:ember": "ember test --test-port=0", 32 | "npm-publish": "release-it" 33 | }, 34 | "dependencies": { 35 | "ember-cli-babel": "^7.26.6", 36 | "ember-cli-htmlbars": "^5.7.1", 37 | "ember-cli-typescript": "^3.1.4" 38 | }, 39 | "peerDependencies": { 40 | "xstate": "^4.12.0" 41 | }, 42 | "devDependencies": { 43 | "@ember/optional-features": "^2.0.0", 44 | "@ember/render-modifiers": "^1.0.2", 45 | "@ember/test-helpers": "^2.4.2", 46 | "@glimmer/component": "^1.0.4", 47 | "@glimmer/manager": "^0.83.1", 48 | "@glimmer/runtime": "^0.83.1", 49 | "@glimmer/tracking": "^1.0.4", 50 | "@glimmer/validator": "^0.83.1", 51 | "@mapbox/rehype-prism": "^0.6.0", 52 | "@release-it/conventional-changelog": "^1.1.4", 53 | "@types/ember": "^3.16.5", 54 | "@types/ember-qunit": "^3.4.9", 55 | "@types/ember__test-helpers": "^1.7.2", 56 | "@types/qunit": "^2.9.1", 57 | "@types/rsvp": "^4.0.3", 58 | "@typescript-eslint/eslint-plugin": "^4.0.1", 59 | "@typescript-eslint/parser": "^4.0.1", 60 | "autoprefixer": "^10.2.5", 61 | "babel-eslint": "^10.1.0", 62 | "broccoli-asset-rev": "^3.0.0", 63 | "ember-angle-bracket-invocation-polyfill": "^2.0.2", 64 | "ember-cli": "~3.28.4", 65 | "ember-auto-import": "^1.11.3", 66 | "ember-cli-dependency-checker": "^3.2.0", 67 | "ember-cli-inject-live-reload": "^2.1.0", 68 | "ember-cli-sri": "^2.1.1", 69 | "ember-cli-terser": "^4.0.2", 70 | "ember-cli-typescript-blueprints": "^3.0.0", 71 | "ember-concurrency": "^1.2.1", 72 | "ember-concurrency-async": "^0.3.1", 73 | "ember-concurrency-decorators": "^2.0.1", 74 | "ember-concurrency-ts": "^0.2.0", 75 | "ember-disable-prototype-extensions": "^1.1.3", 76 | "ember-export-application-global": "^2.0.1", 77 | "ember-load-initializers": "^2.1.2", 78 | "ember-maybe-import-regenerator": "^0.1.6", 79 | "ember-modifier": "^2.1.1", 80 | "ember-page-title": "^6.2.2", 81 | "ember-qunit": "^5.1.4", 82 | "ember-qunit-assert-helpers": "^0.2.2", 83 | "ember-resolver": "^8.0.2", 84 | "ember-source": "~3.28.0", 85 | "ember-template-lint": "^3.6.0", 86 | "eslint": "^7.32.0", 87 | "eslint-config-prettier": "^8.3.0", 88 | "eslint-plugin-ember": "^10.5.4", 89 | "eslint-plugin-node": "^11.1.0", 90 | "eslint-plugin-prettier": "^3.4.1", 91 | "eslint-plugin-qunit": "^6.2.0", 92 | "loader.js": "^4.7.0", 93 | "npm-run-all": "^4.1.5", 94 | "prettier": "^2.3.2", 95 | "qunit": "^2.16.0", 96 | "qunit-dom": "^1.6.0", 97 | "release-it": "^13.6.2", 98 | "typedoc": "^0.20.36", 99 | "typescript": "^4.0.2" 100 | }, 101 | "engines": { 102 | "node": "12.* || 14.* || >= 16" 103 | }, 104 | "changelog": { 105 | "repo": "levelbossmike/ember-statecharts", 106 | "labels": { 107 | ":boom: Breaking": ":boom: Breaking Change", 108 | "Feature / Enhancement": ":rocket: Feature / Enhancement", 109 | "Bug": ":bug: Bug Fix", 110 | "Documentation": ":memo: Documentation", 111 | "internal": ":house: Internal" 112 | } 113 | }, 114 | "ember": { 115 | "edition": "octane" 116 | }, 117 | "ember-addon": { 118 | "configPath": "tests/dummy/config" 119 | }, 120 | "release-it": { 121 | "git": { 122 | "commitMessage": "chore: release v${version}" 123 | }, 124 | "github": { 125 | "release": true 126 | }, 127 | "plugins": { 128 | "@release-it/conventional-changelog": { 129 | "preset": "angular", 130 | "infile": "CHANGELOG.md" 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /packages/ember-statecharts/-private/usables.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Interpreter, 3 | StateSchema, 4 | EventObject, 5 | Typestate, 6 | StateMachine, 7 | State, 8 | SingleOrArray, 9 | Event, 10 | EventData, 11 | InterpreterOptions, 12 | } from 'xstate'; 13 | import { TemplateArgs, Resource } from './resource'; 14 | export declare const ARGS_STATE_CHANGE_WARNING = 15 | 'A change to passed `args` or a local state change triggered an update to a `useMachine`-usable. You can send a dedicated event to the machine or restart it so this is handled. This is done via the `.update`-hook of the `useMachine`-usable.'; 16 | interface StatechartArgs< 17 | TContext, 18 | TStateSchema extends StateSchema, 19 | TEvent extends EventObject, 20 | TTypestate extends Typestate = { 21 | value: any; 22 | context: TContext; 23 | } 24 | > extends TemplateArgs { 25 | named: { 26 | machine: StateMachine; 27 | update?(opts: { 28 | machine: StateMachine; 29 | restart( 30 | args: StatechartArgs 31 | ): void; 32 | send( 33 | event: SingleOrArray> | Event, 34 | payload?: EventData 35 | ): void; 36 | }): void; 37 | onTransition?(): void; 38 | interpreterOptions?: InterpreterOptions; 39 | initialState?: State; 40 | }; 41 | } 42 | /** 43 | * 44 | * A [resource](https://www.pzuraq.com/introducing-use/) that starts 45 | * a [XState-interpreter](https://xstate.js.org/docs/guides/interpretation.html) 46 | * when accessed and will allow handling args-updates when args change after 47 | * the interpreter has started. 48 | * 49 | */ 50 | export declare class Statechart< 51 | TContext, 52 | TStateSchema extends StateSchema, 53 | TEvent extends EventObject, 54 | TTypestate extends Typestate = { 55 | value: any; 56 | context: TContext; 57 | } 58 | > extends Resource> { 59 | service: Interpreter; 60 | state: State; 61 | services: Interpreter[]; 62 | constructor( 63 | owner: Record, 64 | args: StatechartArgs 65 | ); 66 | setup(): void; 67 | update( 68 | args: StatechartArgs 69 | ): void; 70 | teardown(): void; 71 | send( 72 | event: SingleOrArray> | Event, 73 | payload?: EventData 74 | ): void; 75 | _start( 76 | initialState?: State 77 | ): void; 78 | _interpretMachine( 79 | machine: StateMachine, 80 | opts?: InterpreterOptions 81 | ): Interpreter; 82 | _restart( 83 | machine: StateMachine, 84 | initialState?: State 85 | ): void; 86 | _stopOldService(): void; 87 | _rememberInterpretedService( 88 | service: Interpreter 89 | ): void; 90 | } 91 | /** 92 | * A function that can be used to create a {@link Statechart}-resource 93 | * that starts a [XState-interpreter](https://xstate.js.org/docs/guides/interpretation.html) 94 | * when accessed and makes the state of the interpreter accessible to the calling 95 | * context. 96 | * 97 | * ```js 98 | * export default class Button extends Component { 99 | * statechart = useMachine(this, () => { 100 | * return { 101 | * machine: toggleMachine 102 | * } 103 | * }) 104 | * 105 | * @action toggleButton() { 106 | * this.statechart.send('TOGGLE'); 107 | * } 108 | * } 109 | * ``` 110 | */ 111 | export declare function useMachine< 112 | TContext, 113 | TStateSchema extends StateSchema, 114 | TEvent extends EventObject, 115 | TTypestate extends Typestate = { 116 | value: any; 117 | context: TContext; 118 | } 119 | >( 120 | context: object, 121 | computeArgs: () => { 122 | machine: StateMachine; 123 | update?(opts: { 124 | machine: StateMachine; 125 | restart( 126 | args: StatechartArgs 127 | ): void; 128 | send( 129 | event: SingleOrArray> | Event, 130 | payload?: EventData 131 | ): void; 132 | }): void; 133 | onTransition?(): void; 134 | interpreterOptions?: InterpreterOptions; 135 | initialState?: State; 136 | } 137 | ): Statechart; 138 | export {}; 139 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/unit/statechart-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import { module, test } from 'qunit'; 3 | import { statechart } from 'ember-statecharts/computed'; 4 | import { timeout } from 'ember-concurrency'; 5 | import { run } from '@ember/runloop'; 6 | 7 | module('Unit | computed | statechart', function () { 8 | test('it adds statechart functionality to an ember-object', async function (assert) { 9 | assert.expect(5); 10 | 11 | class TestClass extends EmberObject { 12 | @statechart( 13 | { 14 | initial: 'new', 15 | states: { 16 | new: { 17 | exit() { 18 | assert.ok(true, 'exitState was called'); 19 | }, 20 | on: { 21 | woot: { 22 | target: 'foo', 23 | actions: ['handleFoo'], 24 | }, 25 | }, 26 | }, 27 | foo: { 28 | // eslint-disable-next-line no-unused-vars 29 | entry(_context, { type, ...data }) { 30 | assert.deepEqual( 31 | data, 32 | testData, 33 | 'passed data is available in eventObject' 34 | ); 35 | }, 36 | }, 37 | }, 38 | }, 39 | { 40 | actions: { 41 | handleFoo() { 42 | assert.ok(true, 'event was called'); 43 | }, 44 | }, 45 | } 46 | ) 47 | statechart; 48 | } 49 | let subject = TestClass.create(); 50 | 51 | const testData = { wat: 'lol' }; 52 | 53 | assert.equal(subject.statechart.currentState.value, 'new'); 54 | 55 | subject.statechart.send('woot', testData); 56 | 57 | assert.equal(subject.statechart.currentState.value, 'foo'); 58 | }); 59 | 60 | test('it is possible to pass statechart-options to the statechart when passing an array of params', async function (assert) { 61 | assert.expect(5); 62 | 63 | class TestClass extends EmberObject { 64 | name = 'Tomster'; 65 | 66 | power = null; 67 | 68 | @statechart( 69 | { 70 | initial: 'powerOff', 71 | states: { 72 | powerOff: { 73 | on: { 74 | power: { 75 | target: 'powerOn', 76 | cond: 'enoughPowerIsAvailable', 77 | }, 78 | }, 79 | }, 80 | powerOn: {}, 81 | }, 82 | }, 83 | { 84 | guards: { 85 | enoughPowerIsAvailable: (context, { power }) => { 86 | assert.equal(context.name, 'Tomster', 'accessing context works'); 87 | 88 | return power > 9000; 89 | }, 90 | }, 91 | } 92 | ) 93 | statechart; 94 | } 95 | 96 | let subject = TestClass.create(); 97 | 98 | assert.equal( 99 | subject.statechart.currentState.value, 100 | 'powerOff', 101 | 'passing an array as a statechart property works' 102 | ); 103 | 104 | await subject.get('statechart').send('power', { power: 1 }); 105 | 106 | assert.equal( 107 | subject.statechart.currentState.value, 108 | 'powerOff', 109 | 'guards will not execute transition when a falsy value is returned' 110 | ); 111 | 112 | await subject.get('statechart').send('power', { power: 9001 }); 113 | 114 | assert.equal( 115 | subject.statechart.currentState.value, 116 | 'powerOn', 117 | 'returning a truthy from a guard executes the transition' 118 | ); 119 | }); 120 | 121 | test('statechart services will be cleaned properly when the object containing the statechart is destroyed', async function (assert) { 122 | class TestClass extends EmberObject { 123 | offCounter = 0; 124 | 125 | @statechart( 126 | { 127 | initial: 'powerOff', 128 | states: { 129 | powerOff: { 130 | entry: ['incrementOffCounter'], 131 | on: { 132 | POWER: 'powerOn', 133 | }, 134 | }, 135 | powerOn: { 136 | after: { 137 | 1000: 'powerOff', 138 | }, 139 | }, 140 | }, 141 | }, 142 | { 143 | actions: { 144 | incrementOffCounter(context) { 145 | context.incrementProperty('offCounter'); 146 | }, 147 | }, 148 | } 149 | ) 150 | statechart; 151 | 152 | willDestroy() { 153 | super.willDestroy(...arguments); 154 | 155 | assert.step('willDestroy hook is called correctly'); 156 | } 157 | } 158 | 159 | let subject = TestClass.create(); 160 | 161 | assert.equal(subject.statechart.currentState.value, 'powerOff'); 162 | assert.equal( 163 | subject.offCounter, 164 | 1, 165 | 'offCounter was incremented as expected' 166 | ); 167 | 168 | subject.statechart.send('POWER'); 169 | 170 | await timeout(300); 171 | 172 | assert.equal(subject.statechart.currentState.value, 'powerOn'); 173 | 174 | // will fail with `calling set on destroyed object` if this doesn't work 175 | run(() => subject.destroy()); 176 | 177 | assert.verifySteps(['willDestroy hook is called correctly']); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-statecharts [![CI](https://github.com/LevelbossMike/ember-statecharts/actions/workflows/ci.yml/badge.svg)](https://github.com/LevelbossMike/ember-statecharts/actions/workflows/ci.yml) [![Ember Observer Score](https://emberobserver.com/badges/ember-statecharts.svg)](https://emberobserver.com/addons/ember-statecharts) 2 | 3 | This addon provides a statechart abstraction based on [XState](https://xstate.js.org/) 4 | for adding statecharts to your Ember.js application. Statecharts can be used to describe 5 | complex behaviour of your objects and separate ui-concern from behavioral concerns 6 | in your applications. This is especially useful in `Ember.Component`-architecture 7 | but can be used across all layers of your application (e.g. when implementing 8 | global application state). 9 | 10 | [View the docs here.](https://ember-statecharts.com) 11 | 12 | ## Compatibility 13 | 14 | - Ember.js v3.24 or above 15 | - Ember CLI v3.20 or above 16 | - Node.js v12 or above 17 | 18 | For classic Ember.js-versions pre Ember Octane please use the `0.8.x`-version 19 | of this addon. 20 | 21 | For Ember.js versions `< 3.24` please use the `0.13.x`-version of this addon. 22 | 23 | ## Installation 24 | 25 | ``` 26 | ember install ember-statecharts 27 | ``` 28 | 29 | Because ember-statecharts works with [XState](https://xstate.js.org) internally 30 | you have to install it as a dependency as well. 31 | 32 | ``` 33 | yarn add --dev xstate 34 | ``` 35 | 36 | or 37 | 38 | ``` 39 | npm install --save-dev xstate 40 | ``` 41 | 42 | ## Usage 43 | 44 | Statecharts have been around for a long time and have been used to model 45 | stateful, reactive system successfully. You can read about statecharts in the 46 | original paper [Statecharts - A Visual Formalism for Complex 47 | Systems](http://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/resources/statecharts.pdf) 48 | by David Harel. 49 | 50 | With statecharts we finally have a good abstraction to model and discuss behaviour with 51 | other stakeholders of our applications in addition to a design language that 52 | visualizes this behaviour. Here's an example of a button component: 53 | 54 |

55 | button 56 |

57 | 58 | In addition to their modeling capabilities Statecharts are executable and can be used to drive user experience behavior in your Ember.js applications: 59 | 60 | ```js 61 | import Component from '@glimmer/component'; 62 | import { action } from '@ember/object'; 63 | import { task } from 'ember-concurrency'; 64 | 65 | import { matchesState, useMachine } from 'ember-statecharts'; 66 | import { Machine } from 'xstate'; 67 | 68 | function noop() {} 69 | 70 | const buttonMachine = Machine( 71 | { 72 | initial: 'idle', 73 | states: { 74 | idle: { 75 | on: { 76 | SUBMIT: 'busy', 77 | }, 78 | }, 79 | busy: { 80 | entry: ['handleSubmit'], 81 | on: { 82 | SUCCESS: 'success', 83 | ERROR: 'error', 84 | }, 85 | }, 86 | success: { 87 | entry: ['handleSuccess'], 88 | on: { 89 | SUBMIT: 'busy', 90 | }, 91 | }, 92 | error: { 93 | entry: ['handleError'], 94 | on: { 95 | SUBMIT: 'busy', 96 | }, 97 | }, 98 | }, 99 | }, 100 | { 101 | actions: { 102 | handleSubmit() {}, 103 | handleSuccess() {}, 104 | handleError() {}, 105 | }, 106 | } 107 | ); 108 | 109 | export default class QuickstartButton extends Component { 110 | get onClick() { 111 | return this.args.onClick || noop; 112 | } 113 | 114 | statechart = useMachine(this, () => { 115 | const { performSubmitTask, onSuccess, onError } = this; 116 | 117 | return { 118 | machine: quickstartButtonMachine.withConfig({ 119 | actions: { 120 | handleSubmit: performSubmitTask, 121 | handleSuccess: onSuccess, 122 | handleError: onError, 123 | }, 124 | }), 125 | }; 126 | }); 127 | 128 | @matchesState('busy') 129 | isBusy; 130 | 131 | get isDisabled() { 132 | return this.isBusy || this.args.disabled; 133 | } 134 | 135 | @task(function* () { 136 | try { 137 | const result = yield this.onClick(); 138 | this.statechart.send('SUCCESS', { result }); 139 | } catch (e) { 140 | this.statechart.send('ERROR', { error: e }); 141 | } 142 | }) 143 | handleSubmitTask; 144 | 145 | @action 146 | handleClick() { 147 | this.statechart.send('SUBMIT'); 148 | } 149 | 150 | @action 151 | onSuccess(_context, { result }) { 152 | return (this.args.onSuccess && this.args.onSuccess(result)) || noop(); 153 | } 154 | 155 | @action 156 | onError(_context, { error }) { 157 | return (this.args.onError && this.args.onError(error)) || noop(); 158 | } 159 | 160 | @action 161 | performSubmitTask() { 162 | this.handleSubmitTask.perform(); 163 | } 164 | } 165 | ``` 166 | 167 | Please refer to the [documentation page](http://ember-statecharts.com) for a detailed guide of how you can use statecharts to improve your Ember.js application architecture. 168 | 169 | ## Contributing 170 | 171 | See the [Contributing](CONTRIBUTING.md) guide for details. 172 | 173 | ## License 174 | 175 | This project has been developed by https://www.effective-ember.com/ and [contributors](https://github.com/LevelbossMike/ember-statecharts/graphs/contributors). It is licensed under the [MIT License](LICENSE.md). 176 | -------------------------------------------------------------------------------- /packages/site/public/statecharts.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/ember-statecharts/addon/-private/usables.ts: -------------------------------------------------------------------------------- 1 | import { useResource } from './use-resource'; 2 | import { tracked } from '@glimmer/tracking'; 3 | import { interpret } from 'xstate'; 4 | import type { 5 | Interpreter, 6 | StateSchema, 7 | EventObject, 8 | Typestate, 9 | StateMachine, 10 | State, 11 | SingleOrArray, 12 | Event, 13 | EventData, 14 | InterpreterOptions, 15 | } from 'xstate'; 16 | import { TemplateArgs, Resource } from './resource'; 17 | import { warn } from '@ember/debug'; 18 | import { next } from '@ember/runloop'; 19 | 20 | export const ARGS_STATE_CHANGE_WARNING = 21 | 'A change to passed `args` or a local state change triggered an update to a `useMachine`-usable. You can send a dedicated event to the machine or restart it so this is handled. This is done via the `.update`-hook of the `useMachine`-usable.'; 22 | 23 | interface StatechartArgs< 24 | TContext, 25 | TStateSchema extends StateSchema, 26 | TEvent extends EventObject, 27 | TTypestate extends Typestate = { value: any; context: TContext } 28 | > extends TemplateArgs { 29 | named: { 30 | machine: StateMachine; 31 | update?(opts: { 32 | machine: StateMachine; 33 | restart( 34 | args: StatechartArgs 35 | ): void; 36 | send( 37 | event: SingleOrArray> | Event, 38 | payload?: EventData 39 | ): void; 40 | }): void; 41 | onTransition?(): void; 42 | interpreterOptions?: InterpreterOptions; 43 | initialState?: State; 44 | }; 45 | } 46 | 47 | /** 48 | * 49 | * A [resource](https://www.pzuraq.com/introducing-use/) that starts 50 | * a [XState-interpreter](https://xstate.js.org/docs/guides/interpretation.html) 51 | * when accessed and will allow handling args-updates when args change after 52 | * the interpreter has started. 53 | * 54 | */ 55 | export class Statechart< 56 | TContext, 57 | TStateSchema extends StateSchema, 58 | TEvent extends EventObject, 59 | TTypestate extends Typestate = { value: any; context: TContext } 60 | > extends Resource> { 61 | @tracked service: Interpreter; 62 | @tracked state: State; 63 | @tracked services: Interpreter[] = 64 | []; 65 | 66 | constructor( 67 | owner: Record, 68 | args: StatechartArgs 69 | ) { 70 | super(owner, args); 71 | 72 | const machine = args.named.machine; 73 | 74 | const service = this._interpretMachine( 75 | machine, 76 | args.named.interpreterOptions 77 | ); 78 | 79 | this.service = service; 80 | this.state = machine.initialState; 81 | } 82 | 83 | setup() { 84 | const { initialState } = this.args.named; 85 | 86 | this._start(initialState); 87 | } 88 | 89 | update(args: StatechartArgs) { 90 | const { named } = args; 91 | 92 | const { update, machine } = named; 93 | const { _restart, send } = this; 94 | 95 | if (update) { 96 | update({ 97 | machine, 98 | send: send.bind(this), 99 | restart: _restart.bind(this, machine), 100 | }); 101 | } else { 102 | warn(ARGS_STATE_CHANGE_WARNING, false, { 103 | id: 'statecharts.use-machine.args-state-change', 104 | }); 105 | } 106 | } 107 | 108 | teardown() { 109 | this.service.stop(); 110 | } 111 | 112 | send( 113 | event: SingleOrArray> | Event, 114 | payload?: EventData 115 | ) { 116 | this.service.send(event, payload); 117 | } 118 | 119 | _start(initialState?: State) { 120 | this.service.start(initialState).onTransition((state) => { 121 | this.state = state; 122 | }); 123 | 124 | if (this.args.named.onTransition) { 125 | this.service.onTransition(this.args.named.onTransition); 126 | } 127 | } 128 | 129 | _interpretMachine( 130 | machine: StateMachine, 131 | opts?: InterpreterOptions 132 | ) { 133 | const service = interpret(machine, opts); 134 | 135 | return service; 136 | } 137 | 138 | _restart( 139 | machine: StateMachine, 140 | initialState?: State 141 | ) { 142 | const newService = this._interpretMachine(machine); 143 | 144 | this.service = newService; 145 | 146 | this._start(initialState || this.args.named.initialState); 147 | 148 | this._stopOldService(); 149 | } 150 | 151 | _stopOldService() { 152 | const oldService = this.services[this.services.length - 2]; 153 | 154 | if (oldService) { 155 | oldService.stop(); 156 | } 157 | } 158 | 159 | _rememberInterpretedService( 160 | service: Interpreter 161 | ) { 162 | next(this, () => { 163 | this.services = [...this.services, service]; 164 | }); 165 | } 166 | } 167 | 168 | /** 169 | * A function that can be used to create a {@link Statechart}-resource 170 | * that starts a [XState-interpreter](https://xstate.js.org/docs/guides/interpretation.html) 171 | * when accessed and makes the state of the interpreter accessible to the calling 172 | * context. 173 | * 174 | * ```js 175 | * export default class Button extends Component { 176 | * statechart = useMachine(this, () => { 177 | * return { 178 | * machine: toggleMachine 179 | * } 180 | * }) 181 | * 182 | * @action toggleButton() { 183 | * this.statechart.send('TOGGLE'); 184 | * } 185 | * } 186 | * ``` 187 | */ 188 | export function useMachine< 189 | TContext, 190 | TStateSchema extends StateSchema, 191 | TEvent extends EventObject, 192 | TTypestate extends Typestate = { value: any; context: TContext } 193 | >( 194 | context: object, 195 | computeArgs: () => { 196 | machine: StateMachine; 197 | update?(opts: { 198 | machine: StateMachine; 199 | restart( 200 | args: StatechartArgs 201 | ): void; 202 | send( 203 | event: SingleOrArray> | Event, 204 | payload?: EventData 205 | ): void; 206 | }): void; 207 | onTransition?(): void; 208 | interpreterOptions?: InterpreterOptions; 209 | initialState?: State; 210 | } 211 | ): Statechart { 212 | return useResource(context, Statechart, () => { 213 | return { 214 | named: computeArgs(), 215 | }; 216 | }); 217 | } 218 | -------------------------------------------------------------------------------- /packages/ember-statecharts/tests/unit/computed-test.js: -------------------------------------------------------------------------------- 1 | import EmberObject from '@ember/object'; 2 | import { module, test } from 'qunit'; 3 | import { 4 | statechart, 5 | matchesState, 6 | debugState, 7 | } from 'ember-statecharts/computed'; 8 | 9 | module('Unit | statechart computeds', function (hooks) { 10 | hooks.beforeEach(function () { 11 | class TestClass extends EmberObject { 12 | @statechart({ 13 | initial: 'playerOff', 14 | states: { 15 | playerOff: { 16 | on: { 17 | power: 'playerOn', 18 | }, 19 | }, 20 | playerOn: { 21 | initial: 'stopped', 22 | on: { 23 | power: 'playerOff', 24 | }, 25 | states: { 26 | stopped: { 27 | on: { 28 | play: 'playing', 29 | }, 30 | }, 31 | playing: { 32 | on: { 33 | stop: 'stopped', 34 | pause: 'paused', 35 | }, 36 | }, 37 | paused: { 38 | on: { 39 | play: 'playing', 40 | stop: 'stopped', 41 | }, 42 | }, 43 | }, 44 | }, 45 | }, 46 | }) 47 | statechart; 48 | 49 | @statechart({ 50 | initial: 'off', 51 | 52 | states: { 53 | off: { 54 | on: { 55 | POWER: 'on', 56 | }, 57 | }, 58 | on: { 59 | on: { 60 | POWER: 'off', 61 | }, 62 | initial: 'paused', 63 | states: { 64 | paused: { 65 | on: { 66 | START: 'started', 67 | }, 68 | }, 69 | started: { 70 | on: { 71 | PAUSE: 'paused', 72 | }, 73 | }, 74 | }, 75 | }, 76 | }, 77 | }) 78 | secondStatechart; 79 | 80 | @matchesState('playerOff') 81 | playerIsOff; 82 | @matchesState('playerOn') 83 | playerIsOn; 84 | 85 | @matchesState({ 86 | playerOn: 'stopped', 87 | }) 88 | playerIsStopped; 89 | 90 | @matchesState({ 91 | playerOn: 'playing', 92 | }) 93 | playerIsPlaying; 94 | 95 | @matchesState({ 96 | playerOn: 'paused', 97 | }) 98 | playerIsPaused; 99 | 100 | @matchesState([ 101 | { 102 | playerOn: 'stopped', 103 | }, 104 | { 105 | playerOn: 'paused', 106 | }, 107 | ]) 108 | playerActiveMusicNotPlaying; 109 | 110 | @matchesState('off', 'secondStatechart') 111 | secondIsOff; 112 | 113 | @matchesState('on', 'secondStatechart') 114 | secondIsOn; 115 | 116 | @matchesState({ on: 'started' }, 'secondStatechart') 117 | secondIsStarted; 118 | 119 | @debugState() 120 | _debug; 121 | 122 | @debugState('secondStatechart') 123 | _debugSecond; 124 | } 125 | 126 | this.subject = TestClass.create(); 127 | }); 128 | 129 | module('#matchesState', function () { 130 | test('can be used to match against the current state of the statechart', async function (assert) { 131 | let { subject } = this; 132 | 133 | assert.true(subject.playerIsOff, 'works for initial states'); 134 | 135 | await subject.get('statechart').send('power'); 136 | 137 | assert.true(subject.playerIsOn, 'works after updating state'); 138 | 139 | assert.true(subject.playerIsStopped, 'works for nested states'); 140 | 141 | await subject.get('statechart').send('play'); 142 | 143 | assert.false( 144 | subject.playerIsStopped, 145 | 'works inside of nested states - stopped false - playing again' 146 | ); 147 | assert.true( 148 | subject.playerIsPlaying, 149 | 'works inside of nested states - playing true - playing' 150 | ); 151 | 152 | await subject.get('statechart').send('pause'); 153 | 154 | assert.false( 155 | subject.playerIsStopped, 156 | 'works inside of nested states - stopped false - paused' 157 | ); 158 | assert.false( 159 | subject.playerIsPlaying, 160 | 'works inside of nested states - playing false - paused' 161 | ); 162 | assert.true( 163 | subject.playerIsPaused, 164 | 'works inside of nested states - paused true - paused' 165 | ); 166 | 167 | assert.true( 168 | subject.playerActiveMusicNotPlaying, 169 | 'works when passing array' 170 | ); 171 | }); 172 | 173 | test('it can be used with other computeds not named `statechart`', async function (assert) { 174 | let { subject } = this; 175 | 176 | assert.true( 177 | subject.get('secondIsOff'), 178 | 'work for initial state of second statechart' 179 | ); 180 | 181 | await subject.get('secondStatechart').send('POWER'); 182 | 183 | assert.false( 184 | subject.get('secondIsOff'), 185 | 'matchesState updates with custom named statecharts' 186 | ); 187 | 188 | assert.true( 189 | subject.get('secondIsOn'), 190 | 'matchesState computeds work as expected for second statechart' 191 | ); 192 | 193 | await subject.get('secondStatechart').send('START'); 194 | 195 | assert.true( 196 | subject.get('secondIsStarted'), 197 | 'matchesState computeds work as expected for nested states for second statechart' 198 | ); 199 | }); 200 | }); 201 | 202 | module('#debugState', function () { 203 | test('can be used to log the current state of the statechart as a string', async function (assert) { 204 | let { subject } = this; 205 | 206 | assert.deepEqual(subject.get('_debug'), '"playerOff"'); 207 | 208 | await subject.get('statechart').send('power'); 209 | 210 | assert.deepEqual( 211 | subject.get('_debug'), 212 | JSON.stringify({ playerOn: 'stopped' }, 'works for nested states') 213 | ); 214 | 215 | await subject.get('statechart').send('play'); 216 | 217 | assert.deepEqual( 218 | subject.get('_debug'), 219 | JSON.stringify({ playerOn: 'playing' }, 'updates when state is updated') 220 | ); 221 | }); 222 | 223 | test('can be used to log the current state of the statechart as a string - custom name', async function (assert) { 224 | let { subject } = this; 225 | 226 | assert.deepEqual(subject.get('_debugSecond'), '"off"'); 227 | 228 | await subject.get('secondStatechart').send('POWER'); 229 | 230 | assert.deepEqual( 231 | subject.get('_debugSecond'), 232 | JSON.stringify({ on: 'paused' }, 'works for nested states') 233 | ); 234 | 235 | await subject.get('secondStatechart').send('START'); 236 | 237 | assert.deepEqual( 238 | subject.get('_debugSecond'), 239 | JSON.stringify({ on: 'started' }, 'updates when state is updated') 240 | ); 241 | }); 242 | }); 243 | }); 244 | --------------------------------------------------------------------------------