Skip to content

Instantly share code, notes, and snippets.

@stravid
Last active March 23, 2020 16:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stravid/d6f266719b1a7fd9fe205b070ae7e79c to your computer and use it in GitHub Desktop.
Save stravid/d6f266719b1a7fd9fe205b070ae7e79c to your computer and use it in GitHub Desktop.
Statechart Parent Child Activity Example
const { Machine } = XState;
export default Machine({
id: 'child',
initial: 'present',
context: {
name: undefined
},
states: {
present: {
activities: ['nameShowing']
}
}
});
import Ember from 'ember';
export default Ember.Component.extend({
didInsertElement() {
// The child machine is already interpreted, how can I define the activity
// `nameShowing` so the statechart drives the Ember component.
// Returns the passed in child service that got spawned in the parent.
console.log(this.get('childService'));
}
});
import Ember from 'ember';
import parentMachine from '../parent-machine';
const { interpret } = XState;
export default Ember.Component.extend({
didInsertElement() {
const actualMachine = parentMachine.withConfig({
activities: {
emptyListShowing: () => {
this.set('emberStateEmptyListShowing', true);
return () => { this.set('emberStateEmptyListShowing', false) };
},
listShowing: (context, event) => {
this.set('emberStateListShowing', true);
this.set('emberChildren', context.children);
return () => { this.set('emberStateListShowing', false) };
},
inputShowing: () => {
this.set('emberStateInputShowing', true);
return () => { this.set('emberStateInputShowing', false) };
},
}
});
const parentService = interpret(actualMachine);
parentService.start();
this.set('parentService', parentService);
},
actions: {
emberSubmit() {
this.get('parentService').send({
type: 'SUBMIT',
data: { name: this.get('emberChildName') }
});
this.set('emberChildName', '');
}
}
});
import childMachine from './child-machine';
const { Machine, assign, spawn } = XState;
export default Machine({
id: 'parent',
type: 'parallel',
context: {
children: []
},
states: {
list: {
initial: 'sad',
states: {
sad: {
on: {
SUBMIT: { target: 'temp', actions: ['addChild'] }
},
activities: ['emptyListShowing']
},
happy: {
on: {
SUBMIT: { target: 'temp', actions: ['addChild'] }
},
activities: ['listShowing']
},
temp: {
on: {
'': 'happy'
}
}
}
},
form: {
initial: 'input',
states: {
input: {
activities: ['inputShowing']
}
}
}
}
}, {
actions: {
addChild: assign({
children: (context, event) => {
return context.children.concat({
name: event.data.name,
ref: spawn(childMachine.withContext({ name: event.data.name }))
});
}
})
}
});
<p>
There are two statecharts `parent` and `child`. The parent holds a collection of children.
<br>
<br>
There are also two Ember components `my-parent` and `my-child`. The parent extends the parent statechart with activities before interpreting and starting it. The activities are used to control what to display in the UI.
<br>
<br>
The parent Ember component also renders a child Ember component for every interpreted child machine. But the child only gets the already interpreted and started child service. It can't extend the machine with it's own activities bound to itself.
<br>
<br>
I don't think there is a way around this. I struggle to understand how I should do it. My solution (not visible here) is to not use `spawn` and instead interpret the child machine in the child component. But this also means I can't use `sendParent` for example.
</p>
<hr>
{{my-parent}}
{{#if emberNameShowing}}
{{emberName}}
{{/if}}
{{#if emberStateEmptyListShowing}}
<p>
I'm alone :-(
</p>
{{/if}}
{{#if emberStateListShowing}}
<p>
I'm not alone :-)
</p>
<ul>
{{#each emberChildren as |child|}}
<li>Child: {{my-child emberName=child.name childService=child.ref}}</li>
{{/each}}
</ul>
{{/if}}
<hr>
{{#if emberStateInputShowing}}
{{input value=emberChildName placeholder='Enter child name'}}
<button {{action 'emberSubmit'}}>Make parent happy</button>
{{/if}}
import { run } from '@ember/runloop';
export default function destroyApp(application) {
run(application, 'destroy');
}
import Ember from 'ember';
import Application from '../../app';
import config from '../../config/environment';
const { run } = Ember;
const assign = Ember.assign || Ember.merge;
export default function startApp(attrs) {
let application;
let attributes = assign({rootElement: "#test-root"}, config.APP);
attributes.autoboot = true;
attributes = assign(attributes, attrs); // use defaults, but you can override;
run(() => {
application = Application.create(attributes);
application.setupForTesting();
application.injectTestHelpers();
});
return application;
}
import Application from '../app';
import config from '../config/environment';
import { setApplication } from '@ember/test-helpers';
import { start } from 'ember-qunit';
import { assign } from '@ember/polyfills';
let attributes = assign({ rootElement: '#main' }, config.APP);
setApplication(Application.create(attributes));
start();
{
"version": "0.15.1",
"EmberENV": {
"FEATURES": {}
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js",
"ember": "3.4.3",
"ember-template-compiler": "3.4.3",
"ember-testing": "3.4.3",
"xstate": "https://unpkg.com/xstate@4/dist/xstate.js"
},
"addons": {
"ember-data": "3.4.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment