├── .editorconfig ├── .gitignore ├── .npmrc ├── .nvmrc ├── README.md ├── shoppingcart-cli-typescript ├── .browserslistrc ├── .npmrc ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ └── shop.ts │ ├── components │ │ ├── ExampleShop.vue │ │ ├── ProductList.vue │ │ └── ShoppingCart.vue │ ├── main.ts │ ├── models │ │ ├── cart.ts │ │ └── inventory.ts │ ├── plugins │ │ ├── states.ts │ │ └── vuetify.ts │ ├── shims-tsx.d.ts │ └── shims-vue.d.ts ├── tsconfig.json ├── tslint.json └── vue.config.js ├── shoppingcart-cli ├── .browserslistrc ├── .eslintrc.js ├── .npmrc ├── README.md ├── babel.config.js ├── commitlint.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ └── shop.js │ ├── components │ │ ├── ExampleShop.vue │ │ ├── ProductList.vue │ │ └── ShoppingCart.vue │ ├── main.js │ ├── models │ │ ├── __tests__ │ │ │ └── .eslintrc.js │ │ ├── cart.js │ │ ├── index.js │ │ └── inventory.js │ └── plugins │ │ └── vuetify.js └── vue.config.js ├── shoppingcart-nuxt ├── .eslintrc.js ├── .npmrc ├── README.md ├── api │ └── shop.js ├── assets │ └── style │ │ └── app.styl ├── components │ ├── ProductList.vue │ └── ShoppingCart.vue ├── layouts │ └── default.vue ├── models │ ├── cart.js │ ├── index.js │ └── inventory.js ├── nuxt.config.js ├── package-lock.json ├── package.json ├── pages │ └── index.vue ├── plugins │ ├── models.js │ └── vuetify.js └── server │ └── index.js └── todomvc ├── README.md ├── index.html └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | .nuxt 3 | .idea 4 | node_modules 5 | coverage 6 | *.tgz 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.11.0 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue States Examples 2 | 3 | This is a repository with examples showing the use Vue States in combination with Vue History. 4 | 5 | Please refer to the sub directories for information about the different examples. 6 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/README.md: -------------------------------------------------------------------------------- 1 | # Vue States Example - Shoppingcart CLI Typescript 2 | 3 | This example shows how Vue States can be used in a Vue CLI Project with Typescript. 4 | 5 | To run the example 6 | 7 | ```bash 8 | git clone https://github.com/JohannesLamberts/vue-states-examples 9 | cd vue-states-examples/shoppingcart-cli-typescript 10 | npm ci 11 | npm run serve 12 | ``` 13 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shoppingcart-cli-typescript", 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 | "@sum.cumo/vue-history": "1.0.3", 12 | "@sum.cumo/vue-states": "1.1.0", 13 | "core-js": "^2.6.9", 14 | "vue": "^2.6.10", 15 | "vue-class-component": "^7.0.2", 16 | "vue-property-decorator": "^8.1.0", 17 | "vuetify": "^1.5.19" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-plugin-babel": "^3.12.0", 21 | "@vue/cli-plugin-typescript": "^3.6.0", 22 | "@vue/cli-service": "^3.6.0", 23 | "lint-staged": "^8.1.5", 24 | "sass": "^1.18.0", 25 | "sass-loader": "^7.1.0", 26 | "stylus": "^0.54.5", 27 | "stylus-loader": "^3.0.1", 28 | "typescript": "^3.4.3", 29 | "vue-cli-plugin-vuetify": "0.5.0", 30 | "vue-template-compiler": "^2.5.21", 31 | "vuetify-loader": "^1.0.5" 32 | }, 33 | "gitHooks": { 34 | "pre-commit": "lint-staged" 35 | }, 36 | "lint-staged": { 37 | "*.ts": [ 38 | "vue-cli-service lint", 39 | "git add" 40 | ], 41 | "*.vue": [ 42 | "vue-cli-service lint", 43 | "git add" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | shoppingcart-cli-typescript 9 | 10 | 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 30 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/api/shop.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Mocking client-server processing 3 | */ 4 | 5 | export interface Product { 6 | id: number, 7 | title: string, 8 | price: number, 9 | inventory: number, 10 | } 11 | 12 | const PRODUCT_ITEMS: () => Product[] = () => ([ 13 | { 14 | id: 1, title: 'State', price: 500.01, inventory: 2, 15 | }, 16 | { 17 | id: 2, title: 'Management', price: 10.99, inventory: 10, 18 | }, 19 | { 20 | id: 3, title: 'System', price: 19.99, inventory: 5, 21 | }, 22 | ]) 23 | 24 | function wait(ms: number) { 25 | return new Promise(resolve => setTimeout(resolve, ms)) 26 | } 27 | 28 | export default { 29 | async getProducts(): Promise { 30 | await wait(100) 31 | return PRODUCT_ITEMS() 32 | }, 33 | 34 | async buyProducts(): Promise { 35 | await wait(100) 36 | // simulate random checkout failure. 37 | if (Math.random() > 0.5) { 38 | throw new Error('Something went wrong') 39 | } 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/components/ExampleShop.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 43 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/components/ProductList.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 44 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/components/ShoppingCart.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 70 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import './plugins/vuetify' 3 | import './plugins/vuetify' 4 | import './plugins/states' 5 | import App from './App.vue'; 6 | 7 | Vue.config.productionTip = false; 8 | 9 | const vue = new Vue({ 10 | render: (h) => h(App), 11 | }).$mount('#app'); 12 | 13 | // @ts-ignore 14 | window.__VUE__ = vue 15 | // @ts-ignore 16 | window.__VUE_HISTORY__ = vue.$globalHistory 17 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/models/cart.ts: -------------------------------------------------------------------------------- 1 | import shop from '@/api/shop.ts' 2 | import Inventory from '@/models/inventory' 3 | import Vue from 'vue' 4 | import Component from 'vue-class-component' 5 | 6 | interface CartItem { 7 | id: number, 8 | quantity: number, 9 | } 10 | 11 | @Component({ 12 | injectModels: [ 13 | 'Inventory', 14 | ], 15 | }) 16 | export default class Cart extends Vue { 17 | // @ts-ignore 18 | Inventory: Inventory 19 | 20 | items: CartItem[] = [] 21 | checkoutStatus: 'failed' | 'successful' | null = null 22 | 23 | get hasProducts(): boolean { 24 | return this.items.length !== 0 25 | } 26 | 27 | get cartProducts() { 28 | return this.items.map(({ id, quantity }) => { 29 | // todo: resolve inventory 30 | const product = this.Inventory.products.find(productEl => productEl.id === id)! 31 | return { 32 | id: product.id, 33 | title: product.title, 34 | price: product.price, 35 | quantity, 36 | } 37 | }) 38 | } 39 | 40 | get total() { 41 | return this.cartProducts 42 | .reduce((total, product) => total + product.price * product.quantity, 0) 43 | } 44 | 45 | async checkout() { 46 | const savedCartItems = [...this.items] 47 | this.checkoutStatus = null 48 | // empty cart 49 | this.items = [] 50 | try { 51 | await shop.buyProducts() 52 | this.onCheckoutSuccess() 53 | } catch (e) { 54 | this.onCheckoutFailed(savedCartItems) 55 | throw e 56 | } 57 | } 58 | 59 | addProductToCart(productId: number) { 60 | const countItems = 1 61 | this.checkoutStatus = null 62 | const cartItem = this.items.find(item => item.id === productId) 63 | 64 | this.Inventory.modifyProductInventory(productId, -countItems) 65 | 66 | if (!cartItem) { 67 | this.pushProduct(productId) 68 | } else { 69 | this.modifyItemQuantity(productId, countItems) 70 | } 71 | } 72 | 73 | removeProductFromCart(productId: number) { 74 | this.checkoutStatus = null 75 | const cartItem = this.items.find(({ id }) => id === productId) 76 | 77 | if (!cartItem) { 78 | throw new Error(`CartItem ${productId} not found`) 79 | } 80 | this.Inventory.modifyProductInventory(productId, 1) 81 | 82 | if (cartItem.quantity === 1) { 83 | this.removeProduct(productId) 84 | } else { 85 | this.modifyItemQuantity(cartItem.id, -1) 86 | } 87 | } 88 | 89 | /** 90 | * @private 91 | */ 92 | onCheckoutSuccess() { 93 | this.checkoutStatus = 'successful' 94 | } 95 | 96 | /** 97 | * @private 98 | */ 99 | onCheckoutFailed(savedItems: CartItem[]) { 100 | this.checkoutStatus = 'failed' 101 | this.items.push(...savedItems) 102 | } 103 | 104 | /** 105 | * @private 106 | */ 107 | modifyItemQuantity(id: number, modify: number) { 108 | const cartItem = this.items.find(item => item.id === id) 109 | if (!cartItem) { 110 | throw new Error(`CartItem ${id} not found`) 111 | } 112 | cartItem.quantity += modify 113 | } 114 | 115 | /** 116 | * @private 117 | */ 118 | pushProduct(id: number) { 119 | this.items.push({ 120 | id, 121 | quantity: 1, 122 | }) 123 | } 124 | 125 | /** 126 | * @private 127 | */ 128 | removeProduct(removeId: number) { 129 | this.items = this.items.filter(({ id }) => id !== removeId) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/models/inventory.ts: -------------------------------------------------------------------------------- 1 | import shop, { Product } from '@/api/shop.ts' 2 | import Vue from 'vue' 3 | import Component from 'vue-class-component' 4 | 5 | @Component({}) 6 | export default class Inventory extends Vue { 7 | 8 | products: Product[] = [] 9 | loaded = false 10 | 11 | created(): void { 12 | this.loadProducts() 13 | } 14 | 15 | get productMap() { 16 | return new Map(this.products.map(product => ([product.id, product]))) 17 | } 18 | 19 | async loadProducts() { 20 | if (this.loaded) { 21 | throw new Error('Can\'t reload products as quantity would be load') 22 | } 23 | const { saveProducts } = this 24 | saveProducts(await shop.getProducts()) 25 | } 26 | 27 | modifyProductInventory(id: number, mod: number) { 28 | const product = this.products.find(productEl => productEl.id === id) 29 | if (!product || (product.inventory + mod < 0)) { 30 | throw new Error(`Not enough items left for id '${id}'`) 31 | } 32 | product.inventory += mod 33 | } 34 | 35 | /** 36 | * @private 37 | */ 38 | saveProducts(products: Product[]) { 39 | this.products = products 40 | this.loaded = true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/plugins/states.ts: -------------------------------------------------------------------------------- 1 | import VueHistory from '@sum.cumo/vue-history' 2 | import { Event } from '@sum.cumo/vue-history/dist/esm/types' 3 | import VueStates from '@sum.cumo/vue-states' 4 | import Vue from 'vue' 5 | 6 | Vue.use(VueHistory, { 7 | feed: true, 8 | // strict: process.env.NODE_ENV !== 'production', 9 | onEvent: (callEvent: Event) => { 10 | // look for methods being finished before they fired all sub-methods 11 | if (callEvent.caller && callEvent.caller.done) { 12 | console.warn( 13 | 'Method was called after parent method did already finish. Did you forget to await for setTimeout()?', 14 | { event: callEvent }, 15 | ) 16 | } 17 | // look for methods being finished before all fired sub-methods where finished as well 18 | callEvent.promise 19 | .then(() => { 20 | // search for unresolved subEvents 21 | const pending = callEvent.subEvents.filter(e => !e.done) 22 | if (pending.length) { 23 | console.warn( 24 | `Method resolved with ${pending.length} unfinished nested calls. Did you forget to await?`, 25 | { event: callEvent, pending }, 26 | ) 27 | } 28 | }) 29 | }, 30 | }) 31 | 32 | Vue.use(VueStates, { 33 | // restoreOnReplace: true, 34 | mixins: [ 35 | { history: true }, 36 | ], 37 | }) 38 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/plugins/vuetify.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify' 3 | import 'vuetify/src/stylus/app.styl' 4 | 5 | Vue.use(Vuetify, { 6 | iconfont: 'md', 7 | }) 8 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ] 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "tests/**/*.ts", 34 | "tests/**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**" 9 | ] 10 | }, 11 | "rules": { 12 | "quotemark": [true, "single"], 13 | "indent": [true, "spaces", 2], 14 | "interface-name": false, 15 | "ordered-imports": false, 16 | "object-literal-sort-keys": false, 17 | "no-consecutive-blank-lines": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /shoppingcart-cli-typescript/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false 3 | } 4 | -------------------------------------------------------------------------------- /shoppingcart-cli/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /shoppingcart-cli/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/airbnb', 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'semi': [ 14 | 2, 15 | 'never', 16 | ], 17 | 'import/extensions': ['error', 'always', { 18 | 'js': 'never', 19 | 'vue': 'never', 20 | }], 21 | }, 22 | parserOptions: { 23 | parser: 'babel-eslint', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /shoppingcart-cli/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /shoppingcart-cli/README.md: -------------------------------------------------------------------------------- 1 | # Vue States Example - Shoppingcart CLI 2 | 3 | This is a Vue CLI based example, showing a Cart and an Inventory model interacting with each other. 4 | 5 | To run the example 6 | 7 | ```bash 8 | git clone https://github.com/JohannesLamberts/vue-states-examples 9 | cd vue-states-examples/shoppingcart-cli 10 | npm ci 11 | npm run serve 12 | ``` 13 | -------------------------------------------------------------------------------- /shoppingcart-cli/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app', 4 | ], 5 | } 6 | -------------------------------------------------------------------------------- /shoppingcart-cli/commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | parserPreset: { 4 | parserOpts: { 5 | issuePrefixes: ['#'], 6 | }, 7 | }, 8 | } 9 | -------------------------------------------------------------------------------- /shoppingcart-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "scripts": { 4 | "serve": "vue-cli-service serve", 5 | "build": "vue-cli-service build", 6 | "lint": "vue-cli-service lint" 7 | }, 8 | "dependencies": { 9 | "@sum.cumo/vue-history": "1.0.3", 10 | "@sum.cumo/vue-states": "1.0.2", 11 | "babel-polyfill": "6.26.0", 12 | "vue": "2.6.10", 13 | "vuetify": "1.5.10" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "3.5.5", 17 | "@vue/cli-plugin-eslint": "3.5.1", 18 | "@vue/cli-service": "3.5.3", 19 | "@vue/eslint-config-airbnb": "4.0.1", 20 | "babel-core": "7.0.0-bridge.0", 21 | "babel-eslint": "10.1.0", 22 | "eslint": "5.16.0", 23 | "eslint-plugin-vue": "5.2.2", 24 | "stylus": "0.54.5", 25 | "stylus-loader": "3.0.2", 26 | "vue-cli-plugin-vuetify": "0.5.0", 27 | "vue-template-compiler": "2.6.10", 28 | "vuetify-loader": "1.2.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /shoppingcart-cli/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /shoppingcart-cli/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue States Example 8 | 9 | 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /shoppingcart-cli/src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 24 | -------------------------------------------------------------------------------- /shoppingcart-cli/src/api/shop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mocking client-server processing 3 | */ 4 | const PRODUCT_ITEMS = () => ([ 5 | { 6 | id: 1, title: 'State', price: 500.01, inventory: 2, 7 | }, 8 | { 9 | id: 2, title: 'Management', price: 10.99, inventory: 10, 10 | }, 11 | { 12 | id: 3, title: 'System', price: 19.99, inventory: 5, 13 | }, 14 | ]) 15 | 16 | function wait(ms) { 17 | return new Promise(resolve => setTimeout(resolve, ms)) 18 | } 19 | 20 | export default { 21 | async getProducts() { 22 | await wait(100) 23 | return PRODUCT_ITEMS() 24 | }, 25 | 26 | async buyProducts() { 27 | await wait(100) 28 | // simulate random checkout failure. 29 | if (Math.random() > 0.5) { 30 | throw new Error('Something went wrong') 31 | } 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /shoppingcart-cli/src/components/ExampleShop.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 39 | -------------------------------------------------------------------------------- /shoppingcart-cli/src/components/ProductList.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /shoppingcart-cli/src/components/ShoppingCart.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 65 | -------------------------------------------------------------------------------- /shoppingcart-cli/src/main.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import 'babel-polyfill' 3 | import Vue from 'vue' 4 | import VueHistory from '@sum.cumo/vue-history' 5 | import VueStates from '@sum.cumo/vue-states' 6 | import './plugins/vuetify' 7 | import App from './App' 8 | 9 | Vue.use(VueHistory, { 10 | feed: true, 11 | // strict: process.env.NODE_ENV !== 'production', 12 | onEvent: (callEvent) => { 13 | // look for methods being finished before they fired all sub-methods 14 | if (callEvent.caller && callEvent.caller.done) { 15 | console.warn( 16 | 'Method was called after parent method did already finish. Did you forget to await for setTimeout()?', 17 | { event: callEvent }, 18 | ) 19 | } 20 | // look for methods being finished before all fired sub-methods where finished as well 21 | callEvent.promise 22 | .then(() => { 23 | // search for unresolved subEvents 24 | const pending = callEvent.subEvents.filter(e => !e.done) 25 | if (pending.length) { 26 | console.warn( 27 | `Method resolved with ${pending.length} unfinished nested calls. Did you forget to await?`, 28 | { event: callEvent, pending }, 29 | ) 30 | } 31 | }) 32 | }, 33 | }) 34 | 35 | Vue.use(VueStates, { 36 | // restoreOnReplace: true, 37 | mixins: [ 38 | { history: true }, 39 | ], 40 | }) 41 | 42 | Vue.config.productionTip = false 43 | 44 | const vue = new Vue({ 45 | el: '#app', 46 | render: h => h(App), 47 | }) 48 | 49 | window.__VUE__ = vue 50 | window.__VUE_HISTORY__ = vue.$globalHistory 51 | -------------------------------------------------------------------------------- /shoppingcart-cli/src/models/__tests__/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /shoppingcart-cli/src/models/cart.js: -------------------------------------------------------------------------------- 1 | import shop from '@/api/shop' 2 | 3 | export default { 4 | injectModels: [ 5 | 'Inventory', 6 | ], 7 | data() { 8 | return { 9 | items: [], 10 | checkoutStatus: null, 11 | } 12 | }, 13 | computed: { 14 | hasProducts() { 15 | return this.items.length !== 0 16 | }, 17 | 18 | cartProducts() { 19 | return this.items.map(({ id, quantity }) => { 20 | const product = this.Inventory.products.find(productEl => productEl.id === id) 21 | return { 22 | id: product.id, 23 | title: product.title, 24 | price: product.price, 25 | quantity, 26 | } 27 | }) 28 | }, 29 | 30 | total() { 31 | return this.cartProducts 32 | .reduce((total, product) => total + product.price * product.quantity, 0) 33 | }, 34 | }, 35 | methods: { 36 | async checkout() { 37 | const savedCartItems = [...this.items] 38 | this.checkoutStatus = null 39 | // empty cart 40 | this.items = [] 41 | try { 42 | await shop.buyProducts(savedCartItems) 43 | this.onCheckoutSuccess() 44 | } catch (e) { 45 | this.onCheckoutFailed(savedCartItems) 46 | throw e 47 | } 48 | }, 49 | 50 | addProductToCart(productId) { 51 | const countItems = 1 52 | this.checkoutStatus = null 53 | const cartItem = this.items.find(item => item.id === productId) 54 | 55 | this.Inventory.modifyProductInventory(productId, -countItems) 56 | 57 | // TODO: nachgträglich eingefügte subEvent 58 | 59 | if (!cartItem) { 60 | this.pushProduct(productId) 61 | } else { 62 | this.modifyItemQuantity(productId, countItems) 63 | } 64 | }, 65 | 66 | removeProductFromCart(productId) { 67 | this.checkoutStatus = null 68 | const cartItem = this.items.find(({ id }) => id === productId) 69 | this.Inventory.modifyProductInventory(productId, 1) 70 | if (cartItem.quantity === 1) { 71 | this.removeProduct(productId) 72 | } else { 73 | this.modifyItemQuantity(cartItem.id, -1) 74 | } 75 | }, 76 | 77 | /** 78 | * @private 79 | */ 80 | onCheckoutSuccess() { 81 | this.checkoutStatus = 'successful' 82 | }, 83 | 84 | /** 85 | * @private 86 | */ 87 | onCheckoutFailed(savedItems) { 88 | this.checkoutStatus = 'failed' 89 | this.items.push(...savedItems) 90 | }, 91 | 92 | /** 93 | * @private 94 | */ 95 | modifyItemQuantity(id, modify) { 96 | const cartItem = this.items.find(item => item.id === id) 97 | cartItem.quantity += modify 98 | }, 99 | 100 | /** 101 | * @private 102 | */ 103 | pushProduct(id) { 104 | this.items.push({ 105 | id, 106 | quantity: 1, 107 | }) 108 | }, 109 | 110 | /** 111 | * @private 112 | */ 113 | removeProduct(removeId) { 114 | this.items = this.items.filter(({ id }) => id !== removeId) 115 | }, 116 | }, 117 | } 118 | -------------------------------------------------------------------------------- /shoppingcart-cli/src/models/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JohannesLamberts/vue-states-examples/43454467de1cb1a0b1a505dd71a10c19f68d1612/shoppingcart-cli/src/models/index.js -------------------------------------------------------------------------------- /shoppingcart-cli/src/models/inventory.js: -------------------------------------------------------------------------------- 1 | import shop from '@/api/shop' 2 | 3 | export default { 4 | data() { 5 | return { 6 | products: [], 7 | loaded: false, 8 | } 9 | }, 10 | history: true, 11 | created() { 12 | this.loadProducts() 13 | }, 14 | computed: { 15 | productMap() { 16 | return new Map(this.products.map(product => ([product.id, product]))) 17 | }, 18 | }, 19 | methods: { 20 | async loadProducts() { 21 | if (this.loaded) { 22 | throw new Error('Can\'t reload products as quantity would be load') 23 | } 24 | const { saveProducts } = this 25 | saveProducts(await shop.getProducts()) 26 | }, 27 | modifyProductInventory(id, mod) { 28 | const product = this.products.find(productEl => productEl.id === id) 29 | const newInventory = product.inventory + mod 30 | if (newInventory < 0) { 31 | throw new Error(`Not enough items left for id '${id}'`) 32 | } 33 | product.inventory = newInventory 34 | }, 35 | /** 36 | * @private 37 | */ 38 | saveProducts(products) { 39 | this.products = products 40 | this.loaded = true 41 | }, 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /shoppingcart-cli/src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify/lib' 3 | import 'vuetify/src/stylus/app.styl' 4 | 5 | Vue.use(Vuetify, { 6 | iconfont: 'md', 7 | }) 8 | -------------------------------------------------------------------------------- /shoppingcart-cli/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: false, 3 | } 4 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | extends: [ 8 | 'plugin:vue/recommended' 9 | ], 10 | // required to lint *.vue files 11 | plugins: [ 12 | 'vue' 13 | ], 14 | // add your custom rules here 15 | rules: { 16 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 17 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' 18 | }, 19 | parserOptions: { 20 | parser: 'babel-eslint' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/README.md: -------------------------------------------------------------------------------- 1 | # Vue States Example - Shoppingcart Nuxt 2 | 3 | This is a Nuxt based example, showing a Cart and an Inventory model interacting with each other. 4 | 5 | To run the example 6 | 7 | ```bash 8 | git clone https://github.com/JohannesLamberts/vue-states-examples 9 | cd vue-states-examples/shoppingcart-nuxt 10 | npm ci 11 | npm run dev 12 | ``` 13 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/api/shop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mocking client-server processing 3 | */ 4 | const PRODUCT_ITEMS = [ 5 | { 6 | id: 1, title: 'State', price: 500.01, inventory: 2, 7 | }, 8 | { 9 | id: 2, title: 'Management', price: 10.99, inventory: 10, 10 | }, 11 | { 12 | id: 3, title: 'System', price: 19.99, inventory: 5, 13 | }, 14 | ] 15 | 16 | function wait(ms) { 17 | return new Promise(resolve => setTimeout(resolve, ms)) 18 | } 19 | 20 | export default { 21 | async getProducts() { 22 | await wait(100) 23 | return PRODUCT_ITEMS 24 | }, 25 | 26 | async buyProducts() { 27 | await wait(100) 28 | // simulate random checkout failure. 29 | if (Math.random() > 0.5) { 30 | throw new Error('Something went wrong') 31 | } 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/assets/style/app.styl: -------------------------------------------------------------------------------- 1 | // Import Vuetify styling 2 | @require '~vuetify/src/stylus/main.styl' 3 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/components/ProductList.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 44 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/components/ShoppingCart.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 82 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/models/cart.js: -------------------------------------------------------------------------------- 1 | import shop from '@/api/shop' 2 | 3 | export default { 4 | injectModels: [ 5 | 'Inventory', 6 | ], 7 | data() { 8 | return { 9 | items: [], 10 | checkoutStatus: null, 11 | } 12 | }, 13 | computed: { 14 | hasProducts() { 15 | return this.items.length !== 0 16 | }, 17 | 18 | cartProducts() { 19 | return this.items.map(({ id, quantity }) => { 20 | const product = this.Inventory.products.find(productEl => productEl.id === id) 21 | return { 22 | id: product.id, 23 | title: product.title, 24 | price: product.price, 25 | quantity, 26 | } 27 | }) 28 | }, 29 | 30 | total() { 31 | return this.cartProducts 32 | .reduce((total, product) => total + product.price * product.quantity, 0) 33 | }, 34 | }, 35 | methods: { 36 | async checkout() { 37 | const savedCartItems = [...this.items] 38 | this.checkoutStatus = null 39 | // empty cart 40 | this.items = [] 41 | try { 42 | await shop.buyProducts(savedCartItems) 43 | this.onCheckoutSuccess() 44 | } catch (e) { 45 | this.onCheckoutFailed(savedCartItems) 46 | throw e 47 | } 48 | }, 49 | 50 | addProductToCart(productId) { 51 | const countItems = 1 52 | this.checkoutStatus = null 53 | const cartItem = this.items.find(item => item.id === productId) 54 | 55 | this.Inventory.modifyProductInventory(productId, -countItems) 56 | 57 | // TODO: nachgträglich eingefügte subEvent 58 | 59 | if (!cartItem) { 60 | this.pushProduct(productId) 61 | } else { 62 | this.modifyItemQuantity(productId, countItems) 63 | } 64 | }, 65 | 66 | removeProductFromCart(productId) { 67 | this.checkoutStatus = null 68 | const cartItem = this.items.find(({ id }) => id === productId) 69 | this.Inventory.modifyProductInventory(productId, 1) 70 | if (cartItem.quantity === 1) { 71 | this.removeProduct(productId) 72 | } else { 73 | this.modifyItemQuantity(cartItem.id, -1) 74 | } 75 | }, 76 | 77 | /** 78 | * @private 79 | */ 80 | onCheckoutSuccess() { 81 | this.checkoutStatus = 'successful' 82 | }, 83 | 84 | /** 85 | * @private 86 | */ 87 | onCheckoutFailed(savedItems) { 88 | this.checkoutStatus = 'failed' 89 | this.items.push(...savedItems) 90 | }, 91 | 92 | /** 93 | * @private 94 | */ 95 | modifyItemQuantity(id, modify) { 96 | const cartItem = this.items.find(item => item.id === id) 97 | cartItem.quantity += modify 98 | }, 99 | 100 | /** 101 | * @private 102 | */ 103 | pushProduct(id) { 104 | this.items.push({ 105 | id, 106 | quantity: 1, 107 | }) 108 | }, 109 | 110 | /** 111 | * @private 112 | */ 113 | removeProduct(removeId) { 114 | this.items = this.items.filter(({ id }) => id !== removeId) 115 | }, 116 | }, 117 | } 118 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/models/index.js: -------------------------------------------------------------------------------- 1 | import Cart from './cart' 2 | import Inventory from './inventory' 3 | 4 | export default { 5 | Cart, 6 | Inventory, 7 | } 8 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/models/inventory.js: -------------------------------------------------------------------------------- 1 | import shop from '@/api/shop' 2 | 3 | export default { 4 | data() { 5 | return { 6 | products: [], 7 | loaded: false, 8 | } 9 | }, 10 | history: true, 11 | created() { 12 | this.loadProducts() 13 | }, 14 | computed: { 15 | productMap() { 16 | return new Map(this.products.map(product => ([product.id, product]))) 17 | }, 18 | }, 19 | methods: { 20 | async loadProducts() { 21 | if (this.loaded) { 22 | throw new Error('Can\'t reload products as quantity would be load') 23 | } 24 | const { saveProducts } = this 25 | saveProducts(await shop.getProducts()) 26 | }, 27 | modifyProductInventory(id, mod) { 28 | const product = this.products.find(productEl => productEl.id === id) 29 | const newInventory = product.inventory + mod 30 | if (newInventory < 0) { 31 | throw new Error(`Not enough items left for id '${id}'`) 32 | } 33 | product.inventory = newInventory 34 | }, 35 | /** 36 | * @private 37 | */ 38 | saveProducts(products) { 39 | this.products = products 40 | this.loaded = true 41 | }, 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/nuxt.config.js: -------------------------------------------------------------------------------- 1 | const pkg = require('./package') 2 | 3 | module.exports = { 4 | 5 | server: { 6 | port: 8000, // default: 3000 7 | host: '192.168.43.164', // default: localhost 8 | }, 9 | 10 | mode: 'universal', 11 | 12 | /* 13 | ** Headers of the page 14 | */ 15 | head: { 16 | title: pkg.name, 17 | meta: [ 18 | { charset: 'utf-8' }, 19 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 20 | { hid: 'description', name: 'description', content: pkg.description } 21 | ], 22 | link: [ 23 | { rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' } 24 | ] 25 | }, 26 | 27 | /* 28 | ** Customize the progress-bar color 29 | */ 30 | loading: { color: '#fff' }, 31 | 32 | /* 33 | ** Global CSS 34 | */ 35 | css: ['~/assets/style/app.styl'], 36 | 37 | /* 38 | ** Plugins to load before mounting the App 39 | */ 40 | plugins: [ 41 | '@/plugins/vuetify', 42 | '@/plugins/models', 43 | ], 44 | 45 | /* 46 | ** Nuxt.js modules 47 | */ 48 | modules: [ 49 | ], 50 | 51 | /* 52 | ** Build configuration 53 | */ 54 | build: { 55 | /* 56 | ** You can extend webpack config here 57 | */ 58 | extend(config, ctx) { 59 | // Run ESLint on save 60 | if (ctx.isDev && ctx.isClient) { 61 | config.module.rules.push({ 62 | enforce: 'pre', 63 | test: /\.(js|vue)$/, 64 | loader: 'eslint-loader', 65 | exclude: /(node_modules)/ 66 | }) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "scripts": { 4 | "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server", 5 | "build": "nuxt build", 6 | "start": "cross-env NODE_ENV=production node server/index.js", 7 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore ." 8 | }, 9 | "dependencies": { 10 | "@sum.cumo/vue-history": "1.0.0", 11 | "@sum.cumo/vue-states": "1.0.2", 12 | "babel-polyfill": "6.26.0", 13 | "cross-env": "5.2.0", 14 | "express": "4.16.4", 15 | "nuxt": "2.8.1", 16 | "vuetify": "1.5.9" 17 | }, 18 | "devDependencies": { 19 | "babel-eslint": "10.0.1", 20 | "eslint": "5.16.0", 21 | "eslint-loader": "2.1.2", 22 | "eslint-plugin-vue": "5.2.2", 23 | "nodemon": "1.18.10", 24 | "stylus": "0.54.5", 25 | "stylus-loader": "3.0.2", 26 | "webpack": "^4.44.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/pages/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 31 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/plugins/models.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueHistory from '@sum.cumo/vue-history' 3 | import VueStates from '@sum.cumo/vue-states' 4 | import globalModels from '@/models' 5 | 6 | Vue.use(VueHistory, { 7 | feed: typeof window !== 'undefined', 8 | }) 9 | 10 | Vue.use(VueStates, { 11 | mixins: [ 12 | { 13 | history: true, 14 | abstract: true, 15 | }, 16 | ], 17 | globalModels, 18 | }) 19 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import Vue from 'vue' 3 | import Vuetify from 'vuetify' 4 | import colors from 'vuetify/es5/util/colors' 5 | 6 | Vue.use(Vuetify, { 7 | theme: { 8 | primary: '#121212', // a color that is not in the material colors palette 9 | accent: colors.grey.darken3, 10 | secondary: colors.amber.darken3, 11 | info: colors.teal.lighten1, 12 | warning: colors.amber.base, 13 | error: colors.deepOrange.accent4, 14 | success: colors.green.accent3 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /shoppingcart-nuxt/server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const consola = require('consola') 3 | const { Nuxt, Builder } = require('nuxt') 4 | const app = express() 5 | const host = process.env.HOST || '127.0.0.1' 6 | const port = process.env.PORT || 3000 7 | 8 | app.set('port', port) 9 | 10 | // Import and Set Nuxt.js options 11 | let config = require('../nuxt.config.js') 12 | config.dev = !(process.env.NODE_ENV === 'production') 13 | // config.mode = 'spa' 14 | 15 | async function start() { 16 | // Init Nuxt.js 17 | const nuxt = new Nuxt(config) 18 | 19 | // Build only in dev mode 20 | if (config.dev) { 21 | const builder = new Builder(nuxt) 22 | await builder.build() 23 | } 24 | 25 | // Give nuxt middleware to express 26 | app.use(nuxt.render) 27 | 28 | // Listen the server 29 | app.listen(port, host) 30 | consola.ready({ 31 | message: `Server listening on http://${host}:${port}`, 32 | badge: true 33 | }) 34 | } 35 | start() 36 | -------------------------------------------------------------------------------- /todomvc/README.md: -------------------------------------------------------------------------------- 1 | # Vue States Example - TodoMVC 2 | 3 | This is a TodoMVC app showing the import from a CDN and the hydration from stored data. 4 | 5 | To run the example 6 | 7 | - `git clone https://github.com/JohannesLamberts/vue-states-example` 8 | - open ./vue-states-examples/index.html in the browser of your choice 9 | 10 | 11 | -------------------------------------------------------------------------------- /todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 |
15 |
16 |

todos

17 | 18 |
19 |
20 | 27 | 28 | 29 |
30 |
31 | 32 | {{ Todos.remaining }} {{ Todos.remaining > 1 ? 'items' : 'item' }} left 33 | 34 | 39 | 46 |
47 |
48 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /todomvc/index.js: -------------------------------------------------------------------------------- 1 | Vue.use(VueStates.default, { 2 | mixins: [{ history: true }], 3 | }) 4 | 5 | Vue.use(VueHistory, { 6 | feed: true, 7 | filter: event => { 8 | return event.callId !== 'save' 9 | }, 10 | }) 11 | 12 | function uid() { 13 | return Math.random().toString(16).substr(2) 14 | } 15 | 16 | // localStorage persistence 17 | const STORAGE_KEY = 'todos-vue-states-1.0' 18 | 19 | Vue.component('todo-input', { 20 | template: ``, 27 | data() { 28 | return { newTodo: '' } 29 | }, 30 | methods: { 31 | save() { 32 | const value = this.newTodo.trim() 33 | if (!value) { 34 | return 35 | } 36 | this.newTodo = '' 37 | this.$emit('create', { 38 | title: value, 39 | completed: false, 40 | }) 41 | }, 42 | }, 43 | }) 44 | 45 | Vue.component('todo-item', { 46 | props: ['todo', 'editing'], 47 | data() { 48 | return { 49 | editValue: '', 50 | } 51 | }, 52 | template: ` 53 |
  • 58 |
    59 | 65 | 66 | 67 |
    68 | 77 |
  • `, 78 | computed: { 79 | value() { 80 | return this.editing ? this.editValue : this.todo.title 81 | }, 82 | }, 83 | methods: { 84 | changeCompleted(e) { 85 | this.$emit('update', { completed: e.target.checked }) 86 | }, 87 | startEdit() { 88 | this.$emit('edit-init') 89 | this.editValue = this.todo.title 90 | }, 91 | done() { 92 | this.editValue = this.editValue.trim() 93 | if (this.editValue) { 94 | this.$emit('update', { title: this.editValue }) 95 | this.finish() 96 | } 97 | }, 98 | finish() { 99 | this.editValue = '' 100 | this.$emit('edit-finish') 101 | }, 102 | }, 103 | }) 104 | 105 | const filters = ['completed', 'active'] 106 | 107 | Vue.component('todo-list', { 108 | 109 | injectModels: [ 110 | 'Todos', 111 | ], 112 | 113 | template: ` 114 | `, 126 | 127 | data() { 128 | return { 129 | filter: 'all', 130 | editing: null, 131 | } 132 | }, 133 | 134 | mounted() { 135 | window.addEventListener('hashchange', this.onHashChange) 136 | this.onHashChange() 137 | this.$on('hook:beforeDestroy', () => { 138 | window.removeEventListener('hashchange', this.onHashChange) 139 | }) 140 | }, 141 | 142 | computed: { 143 | filtered() { 144 | return this.Todos.filteredMap[this.filter] 145 | }, 146 | }, 147 | 148 | methods: { 149 | setFilter(filter) { 150 | this.filter = filter 151 | }, 152 | onHashChange() { 153 | const visibility = window.location.hash.replace(/#\/?/, '') 154 | this.setFilter(filters.includes(visibility) 155 | ? visibility 156 | : 'all', 157 | ) 158 | }, 159 | }, 160 | }) 161 | 162 | let app 163 | 164 | function persist() { 165 | localStorage.setItem(STORAGE_KEY, JSON.stringify(app.$modelRegistry.exportState())) 166 | } 167 | 168 | const Todos = { 169 | data() { 170 | return { items: [] } 171 | }, 172 | 173 | watch: { 174 | items: { 175 | handler: persist, 176 | deep: true, 177 | }, 178 | }, 179 | 180 | computed: { 181 | filteredMap() { 182 | const filtered = { 183 | all: this.items, 184 | active: [], 185 | completed: [], 186 | } 187 | this.items.forEach((todo) => { 188 | filtered[todo.completed ? 'completed' : 'active'].push(todo) 189 | }) 190 | return filtered 191 | }, 192 | allDone() { 193 | return !this.remaining 194 | }, 195 | remaining() { 196 | return this.filteredMap.active.length 197 | } 198 | }, 199 | 200 | methods: { 201 | create(todo) { 202 | this.items.push({ 203 | id: uid(), 204 | ...todo, 205 | }) 206 | }, 207 | 208 | update(id, update) { 209 | const todo = this.items.find(el => el.id === id) 210 | if (todo) { 211 | Object.assign(todo, update) 212 | } 213 | }, 214 | 215 | remove(id) { 216 | this.items.splice(this.items.findIndex(todo => todo.id === id), 1) 217 | }, 218 | 219 | updateAll(update) { 220 | this.items.forEach(function (todo) { 221 | Object.assign(todo, update) 222 | }) 223 | }, 224 | 225 | removeCompleted() { 226 | this.items = this.filteredMap.active 227 | }, 228 | }, 229 | } 230 | 231 | const modelRegistry = new VueStates.Registry() 232 | 233 | const stored = localStorage.getItem(STORAGE_KEY) 234 | 235 | if(stored) { 236 | modelRegistry.importState(JSON.parse(stored)) 237 | } 238 | 239 | // app Vue instance 240 | app = new Vue({ 241 | modelRegistry, 242 | models: { 243 | Todos, 244 | }, 245 | }) 246 | 247 | // mount 248 | app.$mount('#app') 249 | --------------------------------------------------------------------------------