├── xstate-vis.png
├── public
├── favicon.ico
└── index.html
├── src
├── main.js
├── App.vue
└── calculatorStategraph.js
├── README.MD
└── package.json
/xstate-vis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Glutnix/xstate-vue-calculator/HEAD/xstate-vis.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Glutnix/xstate-vue-calculator/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import VueCompositionApi from "@vue/composition-api";
3 |
4 | import App from "./App.vue";
5 |
6 | Vue.config.productionTip = false;
7 |
8 | Vue.use(VueCompositionApi);
9 |
10 | new Vue({
11 | render: h => h(App)
12 | }).$mount("#app");
13 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | codesandbox
9 |
10 |
11 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 | # Xstate Vue Calculator
2 |
3 | Based on https://github.com/mukeshsoni/statechart-calculator, but upgraded to XState 4.x and moved to Vue 2.x with Vue Composition API.
4 |
5 | * [XState 4.x](https://xstate.js.org/docs/)
6 | * [Vue 2.x](https://vuejs.org/v2/guide/)
7 | * [Vue 2.x Composition API](https://github.com/vuejs/composition-api#readme)
8 | * [@xstate/vue add on](https://github.com/davidkpiano/xstate/tree/master/packages/xstate-vue)
9 |
10 | [](https://codesandbox.io/s/github/Glutnix/xstate-vue-calculator/tree/master/?fontsize=14&hidenavigation=1&module=%2Fsrc%2FcalculatorStategraph.js&theme=dark&view=preview)
11 |
12 | 👀 See the [calculator's state machine on the XState Visualizer](https://xstate.js.org/viz/?gist=06b130cce5e745cf605eee8a8a646de5)
13 |
14 | [](https://xstate.js.org/viz/?gist=06b130cce5e745cf605eee8a8a646de5)
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "xstate-vue-calculator",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "@vue/cli-plugin-babel": "4.1.1",
12 | "@vue/composition-api": "0.3.4",
13 | "@xstate/vue": "0.1.0",
14 | "vue": "2.6.10",
15 | "xstate": "4.7.7"
16 | },
17 | "devDependencies": {
18 | "@vue/cli-plugin-eslint": "4.1.1",
19 | "@vue/cli-service": "4.1.1",
20 | "babel-eslint": "^10.0.3",
21 | "eslint": "^6.7.2",
22 | "eslint-plugin-vue": "^6.0.1",
23 | "vue-template-compiler": "^2.6.11"
24 | },
25 | "eslintConfig": {
26 | "root": true,
27 | "env": {
28 | "node": true
29 | },
30 | "extends": [
31 | "plugin:vue/essential",
32 | "eslint:recommended"
33 | ],
34 | "rules": {},
35 | "parserOptions": {
36 | "parser": "babel-eslint"
37 | }
38 | },
39 | "postcss": {
40 | "plugins": {
41 | "autoprefixer": {}
42 | }
43 | },
44 | "browserslist": [
45 | "> 1%",
46 | "last 2 versions",
47 | "not ie <= 8"
48 | ],
49 | "keywords": [],
50 | "description": "Ported to xState 4.x and Vue from \nhttps://github.com/mukeshsoni/statechart-calculator"
51 | }
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 |
15 |
16 |
State
17 |
{{ state.value }}
18 |
Context:
19 |
{{ state.context }}
20 |
21 |
22 |
23 |
24 |
25 |
104 |
105 |
187 |
--------------------------------------------------------------------------------
/src/calculatorStategraph.js:
--------------------------------------------------------------------------------
1 | import { Machine, assign, actions } from "xstate";
2 |
3 | const not = fn => (...args) => !fn.apply(null, args);
4 | const isZero = (context, event) => event.key === 0;
5 | const isNotZero = not(isZero);
6 | const isMinus = (context, event) => event.operator === "-";
7 | const isNotMinus = not(isMinus);
8 | const divideByZero = (context, event) =>
9 | context.operand2 === "0." && context.operator === "/";
10 | const notDivideByZero = not(divideByZero);
11 |
12 | function doMath(operand1, operand2, operator) {
13 | switch (operator) {
14 | case "+":
15 | return +operand1 + +operand2;
16 | case "-":
17 | return +operand1 - +operand2;
18 | case "/":
19 | return +operand1 / +operand2;
20 | case "x":
21 | return +operand1 * +operand2;
22 | default:
23 | return Infinity;
24 | }
25 | }
26 |
27 | const calcMachine = Machine(
28 | {
29 | id: "calcMachine",
30 | context: {
31 | display: "0.",
32 | operand1: null,
33 | operand2: null,
34 | operator: null
35 | },
36 | // strict: true,
37 | on: {
38 | CLEAR_EVERYTHING: {
39 | target: ".start",
40 | actions: ["reset"]
41 | }
42 | },
43 | initial: "start",
44 | states: {
45 | start: {
46 | on: {
47 | NUMBER: [
48 | {
49 | cond: "isZero",
50 | target: "operand1.zero",
51 | actions: ["defaultReadout"]
52 | },
53 | {
54 | cond: "isNotZero",
55 | target: "operand1.before_decimal_point",
56 | actions: ["setReadoutNum"]
57 | }
58 | ],
59 | OPERATOR: {
60 | cond: "isMinus",
61 | target: "negative_number",
62 | actions: ["startNegativeNumber"]
63 | },
64 | DECIMAL_POINT: {
65 | target: "operand1.after_decimal_point",
66 | actions: ["defaultReadout"]
67 | }
68 | }
69 | },
70 | operand1: {
71 | on: {
72 | OPERATOR: {
73 | target: "operator_entered",
74 | actions: ["recordOperator"]
75 | },
76 | PERCENTAGE: {
77 | target: "result",
78 | actions: ["storeResultAsOperand2", "computePercentage"]
79 | },
80 | CLEAR_ENTRY: {
81 | target: "operand1",
82 | actions: ["defaultReadout"]
83 | }
84 | },
85 | initial: "zero",
86 | states: {
87 | zero: {
88 | on: {
89 | NUMBER: {
90 | target: "before_decimal_point",
91 | actions: ["setReadoutNum"]
92 | },
93 | DECIMAL_POINT: "after_decimal_point"
94 | }
95 | },
96 | before_decimal_point: {
97 | on: {
98 | NUMBER: {
99 | target: "before_decimal_point",
100 | actions: ["appendNumBeforeDecimal"]
101 | },
102 | DECIMAL_POINT: "after_decimal_point"
103 | }
104 | },
105 | after_decimal_point: {
106 | on: {
107 | NUMBER: {
108 | target: "after_decimal_point",
109 | actions: ["appendNumAfterDecimal"]
110 | }
111 | }
112 | }
113 | }
114 | },
115 | negative_number: {
116 | on: {
117 | NUMBER: [
118 | {
119 | cond: "isZero",
120 | target: "operand1.zero",
121 | actions: ["defaultNegativeReadout"]
122 | },
123 | {
124 | cond: "isNotZero",
125 | target: "operand1.before_decimal_point",
126 | actions: ["setNegativeReadoutNum"]
127 | }
128 | ],
129 | DECIMAL_POINT: {
130 | target: "operand1.after_decimal_point",
131 | actions: ["defaultNegativeReadout"]
132 | },
133 | CLEAR_ENTRY: {
134 | target: "start",
135 | actions: ["defaultReadout"]
136 | }
137 | }
138 | },
139 | operator_entered: {
140 | on: {
141 | OPERATOR: [
142 | {
143 | cond: "isNotMinus",
144 | target: "operator_entered",
145 | actions: ["setOperator"]
146 | },
147 | {
148 | cond: "isMinus",
149 | target: "negative_number_2",
150 | actions: ["startNegativeNumber"]
151 | }
152 | ],
153 | NUMBER: [
154 | {
155 | target: "operand2.zero",
156 | actions: ["defaultReadout"],
157 | cond: "isZero"
158 | },
159 | {
160 | cond: "isNotZero",
161 | target: "operand2.before_decimal_point",
162 | actions: ["setReadoutNum"]
163 | }
164 | ],
165 | DECIMAL_POINT: {
166 | target: "operand2.after_decimal_point",
167 | actions: ["defaultReadout"]
168 | }
169 | }
170 | },
171 | operand2: {
172 | on: {
173 | OPERATOR: {
174 | target: "operator_entered",
175 | actions: [
176 | "storeResultAsOperand2",
177 | "compute",
178 | "storeResultAsOperand1",
179 | "setOperator"
180 | ]
181 | },
182 | EQUALS: [
183 | {
184 | cond: "notDivideByZero",
185 | target: "result",
186 | actions: ["storeResultAsOperand2", "compute"]
187 | },
188 | { target: "alert", actions: ["divideByZeroAlert"] }
189 | ],
190 | CLEAR_ENTRY: {
191 | target: "operand2",
192 | actions: ["defaultReadout"]
193 | }
194 | },
195 | initial: "hist",
196 | states: {
197 | hist: {
198 | type: "history",
199 | target: "zero"
200 | },
201 | zero: {
202 | on: {
203 | NUMBER: {
204 | target: "before_decimal_point",
205 | actions: ["setReadoutNum"]
206 | },
207 | DECIMAL_POINT: "after_decimal_point"
208 | }
209 | },
210 | before_decimal_point: {
211 | on: {
212 | NUMBER: {
213 | target: "before_decimal_point",
214 | actions: ["appendNumBeforeDecimal"]
215 | },
216 | DECIMAL_POINT: "after_decimal_point"
217 | }
218 | },
219 | after_decimal_point: {
220 | on: {
221 | NUMBER: {
222 | target: "after_decimal_point",
223 | actions: ["appendNumAfterDecimal"]
224 | }
225 | }
226 | }
227 | }
228 | },
229 | negative_number_2: {
230 | on: {
231 | NUMBER: [
232 | {
233 | cond: "isZero",
234 | target: "operand2.zero",
235 | actions: ["defaultNegativeReadout"]
236 | },
237 | {
238 | cond: "isNotZero",
239 | target: "operand2.before_decimal_point",
240 | actions: ["setNegativeReadoutNum"]
241 | }
242 | ],
243 | DECIMAL_POINT: {
244 | target: "operand2.after_decimal_point",
245 | actions: ["defaultNegativeReadout"]
246 | },
247 | CLEAR_ENTRY: {
248 | target: "operator_entered",
249 | actions: ["defaultReadout"]
250 | }
251 | }
252 | },
253 | result: {
254 | on: {
255 | NUMBER: [
256 | {
257 | cond: "isZero",
258 | target: "operand1",
259 | actions: ["defaultReadout"]
260 | },
261 | {
262 | cond: "isNotZero",
263 | target: "operand1.before_decimal_point",
264 | actions: ["setReadoutNum"]
265 | }
266 | ],
267 | PERCENTAGE: {
268 | target: "result",
269 | actions: ["storeResultAsOperand2", "computePercentage"]
270 | },
271 | OPERATOR: {
272 | target: "operator_entered",
273 | actions: ["storeResultAsOperand1", "recordOperator"]
274 | },
275 | CLEAR_ENTRY: {
276 | target: "start",
277 | actions: ["defaultReadout"]
278 | }
279 | }
280 | },
281 | alert: {
282 | on: {
283 | OK: "operand2.hist"
284 | }
285 | }
286 | }
287 | },
288 | {
289 | guards: {
290 | isMinus,
291 | isNotMinus,
292 | isZero,
293 | isNotZero,
294 | notDivideByZero
295 | },
296 | actions: {
297 | defaultReadout: assign({
298 | display: () => "0."
299 | }),
300 |
301 | defaultNegativeReadout: assign({
302 | display: () => "-0."
303 | }),
304 |
305 | appendNumBeforeDecimal: assign({
306 | display: (context, event) =>
307 | context.display.slice(0, -1) + event.key + "."
308 | }),
309 |
310 | appendNumAfterDecimal: assign({
311 | display: (context, event) => context.display + event.key
312 | }),
313 |
314 | setReadoutNum: assign({
315 | display: (context, event) => event.key + "."
316 | }),
317 |
318 | setNegativeReadoutNum: assign({
319 | display: (context, event) => "-" + event.key + "."
320 | }),
321 |
322 | startNegativeNumber: assign({
323 | display: () => "-"
324 | }),
325 |
326 | recordOperator: assign({
327 | operand1: context => context.display,
328 | operator: (_, event) => event.operator
329 | }),
330 |
331 | setOperator: assign({
332 | operator: ({ operator }) => operator
333 | }),
334 |
335 | computePercentage: assign({
336 | display: context => context.display / 100
337 | }),
338 |
339 | compute: assign({
340 | display: ({ operand1, operand2, operator }) => {
341 | const result = doMath(operand1, operand2, operator);
342 | console.log(
343 | `doing calculation ${operand1} ${operator} ${operand2} = ${result}`
344 | );
345 | return result;
346 | }
347 | }),
348 |
349 | storeResultAsOperand1: assign({
350 | operand1: context => context.display
351 | }),
352 |
353 | storeResultAsOperand2: assign({
354 | operand2: context => context.display
355 | }),
356 |
357 | divideByZeroAlert() {
358 | // have to put the alert in setTimeout because action is executed on event, before the transition to next state happens
359 | // this alert is supposed to happend on transition
360 | // setTimeout allows time for other state transition (to 'alert' state) to happen before showing the alert
361 | // probably a better way to do it. like entry or exit actions
362 | setTimeout(() => {
363 | alert("Cannot divide by zero!");
364 | this.transition("OK");
365 | }, 0);
366 | },
367 |
368 | reset: assign({
369 | display: () => "0.",
370 | operand1: () => null,
371 | operand2: () => null,
372 | operator: () => null
373 | })
374 | }
375 | }
376 | );
377 |
378 | export default calcMachine;
379 |
--------------------------------------------------------------------------------