├── .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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
43 |
--------------------------------------------------------------------------------
/shoppingcart-cli-typescript/src/components/ProductList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Products
4 |
5 |
9 |
10 |
11 |
12 |
13 | {{ product.title }}
14 |
15 |
16 | {{ product.price.toFixed(2) }} € - {{ product.inventory }} left
17 |
18 |
19 |
20 |
25 | add_shopping_cart
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
44 |
--------------------------------------------------------------------------------
/shoppingcart-cli-typescript/src/components/ShoppingCart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Your Cart
4 |
8 | Please add some products to cart.
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ product.title }}
16 |
17 |
18 | {{ product.price.toFixed(2) }} € x {{ product.quantity }}
19 | =
20 | {{ product.price * product.quantity.toFixed(2) }} €
21 |
22 |
23 |
24 |
28 | remove_shopping_cart
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Total: {{ Cart.total.toFixed(2) }} €
38 | {{ Cart.checkoutStatus }}.
39 |
40 |
41 | Checkout
42 |
43 |
44 |
45 | Cart.checkout()"
48 | color="primary"
49 | >
50 | Checkout
51 |
52 |
53 |
54 |
55 |
56 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
39 |
--------------------------------------------------------------------------------
/shoppingcart-cli/src/components/ProductList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Products
4 |
5 |
9 |
10 |
11 |
12 |
13 | {{ product.title }}
14 |
15 |
16 | {{ product.price.toFixed(2) }} € - {{ product.inventory }} left
17 |
18 |
19 |
20 |
25 | add_shopping_cart
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
39 |
--------------------------------------------------------------------------------
/shoppingcart-cli/src/components/ShoppingCart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Your Cart
4 |
8 | Please add some products to cart.
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ product.title }}
16 |
17 |
18 | {{ product.price.toFixed(2) }} € x {{ product.quantity }}
19 | =
20 | {{ product.price * product.quantity.toFixed(2) }} €
21 |
22 |
23 |
24 |
28 | remove_shopping_cart
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | Total: {{ Cart.total.toFixed(2) }} €
38 | {{ Cart.checkoutStatus }}.
39 |
40 |
41 | Checkout
42 |
43 |
44 |
45 | Cart.checkout()"
48 | color="primary"
49 | >
50 | Checkout
51 |
52 |
53 |
54 |
55 |
56 |
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 |
2 |
3 |
4 | Products
5 |
6 |
7 |
11 |
12 |
13 |
14 |
15 | {{ product.title }}
16 |
17 |
18 | {{ product.price.toFixed(2) }} € - {{ product.inventory }} left
19 |
20 |
21 |
22 |
27 | add_shopping_cart
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
44 |
--------------------------------------------------------------------------------
/shoppingcart-nuxt/components/ShoppingCart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Your Cart
5 |
6 |
10 | Please add some products to cart.
11 |
12 |
13 |
14 |
15 |
16 |
17 | {{ product.title }}
18 |
19 |
20 | {{ product.price.toFixed(2) }} € x {{ product.quantity }}
21 | =
22 | {{ product.price * product.quantity.toFixed(2) }} €
23 |
24 |
25 |
26 |
30 | remove_shopping_cart
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Total: {{ Cart.total.toFixed(2) }} €
40 | {{ Cart.checkoutStatus }}.
41 |
42 |
43 | Checkout
44 |
45 |
46 |
47 |
52 | Checkout
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
82 |
--------------------------------------------------------------------------------
/shoppingcart-nuxt/layouts/default.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
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 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
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 | editValue = e.target.value"
73 | @blur="done"
74 | @keyup.enter="done"
75 | @keyup.esc="finish"
76 | />
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 |
115 | Todos.update(todo.id, partial)"
123 | @remove="Todos.remove(todo.id)"
124 | />
125 |
`,
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 |
--------------------------------------------------------------------------------