├── 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 | [![Edit xstate-vue-calculator](https://codesandbox.io/static/img/play-codesandbox.svg)](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 | [![Screenshot of the calculator state machine in the Visualizer](xstate-vis.png)](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 | 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 | --------------------------------------------------------------------------------