ソースを参照

feat!: add vue 3 support

BREAKING CHANGE:
- It only works with Vue 3.
- The installation process has been changed due to aligning with Vue 3's new
initiation process.
Kia King Ishii 5 年 前
コミット
6416ebb1e7
100 ファイル変更1627 行追加178 行削除
  1. 20 20
      .circleci/config.yml
  2. 48 19
      README.md
  3. 2 0
      build/build.main.js
  4. 0 19
      examples/chat/app.js
  5. 0 0
      examples/classic/chat/api/index.js
  6. 0 0
      examples/classic/chat/api/mock-data.js
  7. 13 0
      examples/classic/chat/app.js
  8. 0 0
      examples/classic/chat/components/App.vue
  9. 6 1
      examples/classic/chat/components/Message.vue
  10. 0 0
      examples/classic/chat/components/MessageSection.vue
  11. 7 2
      examples/classic/chat/components/Thread.vue
  12. 0 0
      examples/classic/chat/components/ThreadSection.vue
  13. 0 0
      examples/classic/chat/css/chat.css
  14. 1 1
      examples/classic/chat/index.html
  15. 0 0
      examples/classic/chat/store/actions.js
  16. 0 0
      examples/classic/chat/store/getters.js
  17. 3 6
      examples/classic/chat/store/index.js
  18. 13 9
      examples/classic/chat/store/mutations.js
  19. 0 0
      examples/classic/counter-hot/CounterControls.vue
  20. 9 0
      examples/classic/counter-hot/app.js
  21. 1 1
      examples/classic/counter-hot/index.html
  22. 0 0
      examples/classic/counter-hot/store/actions.js
  23. 0 0
      examples/classic/counter-hot/store/getters.js
  24. 2 5
      examples/classic/counter-hot/store/index.js
  25. 0 0
      examples/classic/counter-hot/store/mutations.js
  26. 0 0
      examples/classic/counter/Counter.vue
  27. 10 0
      examples/classic/counter/app.js
  28. 1 1
      examples/classic/counter/index.html
  29. 2 5
      examples/classic/counter/store.js
  30. 0 0
      examples/classic/shopping-cart/api/shop.js
  31. 7 7
      examples/classic/shopping-cart/app.js
  32. 0 0
      examples/classic/shopping-cart/components/App.vue
  33. 8 4
      examples/classic/shopping-cart/components/ProductList.vue
  34. 4 2
      examples/classic/shopping-cart/components/ShoppingCart.vue
  35. 0 0
      examples/classic/shopping-cart/currency.js
  36. 1 1
      examples/classic/shopping-cart/index.html
  37. 3 6
      examples/classic/shopping-cart/store/index.js
  38. 0 0
      examples/classic/shopping-cart/store/modules/cart.js
  39. 0 0
      examples/classic/shopping-cart/store/modules/products.js
  40. 9 0
      examples/classic/todomvc/app.js
  41. 8 6
      examples/classic/todomvc/components/App.vue
  42. 8 2
      examples/classic/todomvc/components/TodoItem.vue
  43. 1 1
      examples/classic/todomvc/index.html
  44. 0 0
      examples/classic/todomvc/store/actions.js
  45. 2 5
      examples/classic/todomvc/store/index.js
  46. 1 1
      examples/classic/todomvc/store/mutations.js
  47. 1 1
      examples/classic/todomvc/store/plugins.js
  48. 24 0
      examples/composition/chat/api/index.js
  49. 58 0
      examples/composition/chat/api/mock-data.js
  50. 13 0
      examples/composition/chat/app.js
  51. 21 0
      examples/composition/chat/components/App.vue
  52. 23 0
      examples/composition/chat/components/Message.vue
  53. 63 0
      examples/composition/chat/components/MessageSection.vue
  54. 29 0
      examples/composition/chat/components/Thread.vue
  55. 39 0
      examples/composition/chat/components/ThreadSection.vue
  56. 98 0
      examples/composition/chat/css/chat.css
  57. 13 0
      examples/composition/chat/index.html
  58. 17 0
      examples/composition/chat/store/actions.js
  59. 25 0
      examples/composition/chat/store/getters.js
  60. 42 0
      examples/composition/chat/store/index.js
  61. 64 0
      examples/composition/chat/store/mutations.js
  62. 32 0
      examples/composition/counter-hot/CounterControls.vue
  63. 9 0
      examples/composition/counter-hot/app.js
  64. 13 0
      examples/composition/counter-hot/index.html
  65. 18 0
      examples/composition/counter-hot/store/actions.js
  66. 15 0
      examples/composition/counter-hot/store/getters.js
  67. 33 0
      examples/composition/counter-hot/store/index.js
  68. 9 0
      examples/composition/counter-hot/store/mutations.js
  69. 35 0
      examples/composition/counter/Counter.vue
  70. 10 0
      examples/composition/counter/app.js
  71. 13 0
      examples/composition/counter/index.html
  72. 55 0
      examples/composition/counter/store.js
  73. 23 0
      examples/composition/shopping-cart/api/shop.js
  74. 13 0
      examples/composition/shopping-cart/app.js
  75. 19 0
      examples/composition/shopping-cart/components/App.vue
  76. 48 0
      examples/composition/shopping-cart/components/ProductList.vue
  77. 42 0
      examples/composition/shopping-cart/components/ShoppingCart.vue
  78. 23 0
      examples/composition/shopping-cart/currency.js
  79. 13 0
      examples/composition/shopping-cart/index.html
  80. 15 0
      examples/composition/shopping-cart/store/index.js
  81. 92 0
      examples/composition/shopping-cart/store/modules/cart.js
  82. 38 0
      examples/composition/shopping-cart/store/modules/products.js
  83. 9 0
      examples/composition/todomvc/app.js
  84. 103 0
      examples/composition/todomvc/components/App.vue
  85. 68 0
      examples/composition/todomvc/components/TodoItem.vue
  86. 12 0
      examples/composition/todomvc/index.html
  87. 33 0
      examples/composition/todomvc/store/actions.js
  88. 13 0
      examples/composition/todomvc/store/index.js
  89. 26 0
      examples/composition/todomvc/store/mutations.js
  90. 12 0
      examples/composition/todomvc/store/plugins.js
  91. 0 9
      examples/counter-hot/app.js
  92. 0 10
      examples/counter/app.js
  93. 18 5
      examples/index.html
  94. 4 0
      examples/server.js
  95. 0 10
      examples/todomvc/app.js
  96. 16 7
      examples/webpack.config.js
  97. 6 5
      package.json
  98. 7 4
      src/index.esm.js
  99. 5 3
      src/index.js
  100. 7 0
      src/injectKey.js

+ 20 - 20
.circleci/config.yml

@@ -27,23 +27,23 @@ jobs:
           paths:
             - vuex
 
-  lint-types:
-    <<: *defaults
-    steps:
-      - attach_workspace:
-          at: ~/
-      - run:
-          name: Linting
-          command: |
-            yarn lint --format junit --output-file test-results/eslint/results.xml
-      - run:
-          name: Testing Types
-          command: |
-            yarn test:types
-      - store_test_results:
-          path: test-results
-      - store_artifacts:
-          path: test-results
+  # lint-types:
+  #   <<: *defaults
+  #   steps:
+  #     - attach_workspace:
+  #         at: ~/
+  #     - run:
+  #         name: Linting
+  #         command: |
+  #           yarn lint --format junit --output-file test-results/eslint/results.xml
+  #     - run:
+  #         name: Testing Types
+  #         command: |
+  #           yarn test:types
+  #     - store_test_results:
+  #         path: test-results
+  #     - store_artifacts:
+  #         path: test-results
 
   test-unit:
     <<: *defaults
@@ -80,9 +80,9 @@ workflows:
   install-and-parallel-test:
     jobs:
       - install
-      - lint-types:
-          requires:
-            - install
+      # - lint-types:
+      #     requires:
+      #       - install
       - test-unit:
           requires:
             - install

+ 48 - 19
README.md

@@ -1,29 +1,58 @@
-# Vuex [![Build Status](https://circleci.com/gh/vuejs/vuex/tree/dev.png?style=shield)](https://circleci.com/gh/vuejs/vuex)
+# Vuex 4
 
-> Centralized State Management for Vue.js.
+This is the Vue 3 compatible version of Vuex. The focus is compatibility, and it provides the exact same API as Vuex 3, so users can reuse their existing Vuex code for Vue 3.
 
-<p align="center">
-  <img width="700px" src="https://raw.githubusercontent.com/vuejs/vuex/dev/docs/.vuepress/public/vuex.png">
-</p>
+## Status: Alpha
 
-- [What is Vuex?](https://vuex.vuejs.org/)
-- [Full Documentation](http://vuex.vuejs.org/)
+All Vuex 3 feature works. There are a few breaking changes described in a later section, so please check them out. You can find basic usage with both option and composition API at `example` folder.
 
-## Examples
+Please note that it's still unstable, and there might be bugs. Please provide us feedback if you find anything. You may use [vue-next-webpack-preview](https://github.com/vuejs/vue-next-webpack-preview) to test out Vue 3 with Vuex 4.
 
-- [Counter](https://github.com/vuejs/vuex/tree/dev/examples/counter)
-- [Counter with Hot Reload](https://github.com/vuejs/vuex/tree/dev/examples/counter-hot)
-- [TodoMVC](https://github.com/vuejs/vuex/tree/dev/examples/todomvc)
-- [Flux Chat](https://github.com/vuejs/vuex/tree/dev/examples/chat)
-- [Shopping Cart](https://github.com/vuejs/vuex/tree/dev/examples/shopping-cart)
+## Breaking changes
 
-Running the examples:
+### Installation process has changed
 
-``` bash
-$ npm install
-$ npm run dev # serve examples at localhost:8080
+To align with the new Vue 3 initialization process, the installation process of Vuex has changed as well.
+
+You should use a new `createStore` function to create a new store instance.
+
+```js
+import { createStore } from 'vuex'
+
+const store = createStore({
+  state () {
+    return {
+      count: 1
+    }
+  }
+})
+```
+
+> This is technically not a breaking change because you could still use `new Store(...)` syntax. However, to align with Vue 3 and also with Vue Router Next, we recommend users to use `createStore` function instead.
+
+Then to install Vuex to Vue app instance, pass the store instance instead of Vuex.
+
+```js
+import { createApp } from 'vue'
+import store from './store'
+import App from './APP.vue'
+
+const app = createApp(Counter)
+
+app.use(store)
+
+app.mount('#app')
 ```
 
-## License
+## Kown issues
+
+- The code is kept as close to Vuex 3 code base as possible, and there're plenty of places where we should refactor. However, we are waiting for all of the test cases to pass before doing so (some tests require Vue 3 update).
+- TypeScript support is not ready yet. Please use JS environment to test this for now.
+
+## TODOs as of 4.0.0-alpha.1
 
-[MIT](http://opensource.org/licenses/MIT)
+- Add TypeScript support
+- Make all unit test working
+- Refactor the codebase
+- Update the build system to align with Vue 3
+- Update docs

+ 2 - 0
build/build.main.js

@@ -27,6 +27,8 @@ function build (builds) {
 }
 
 function buildEntry ({ input, output }) {
+  input.external = ['vue']
+  output.globals = { vue: 'Vue' }
   const { file, banner } = output
   const isProd = /min\.js$/.test(file)
   return rollup.rollup(input)

+ 0 - 19
examples/chat/app.js

@@ -1,19 +0,0 @@
-import 'babel-polyfill'
-import Vue from 'vue'
-import App from './components/App.vue'
-import store from './store'
-import { getAllMessages } from './store/actions'
-
-Vue.config.debug = true
-
-Vue.filter('time', timestamp => {
-  return new Date(timestamp).toLocaleTimeString()
-})
-
-new Vue({
-  el: '#app',
-  store,
-  render: h => h(App)
-})
-
-getAllMessages(store)

+ 0 - 0
examples/chat/api/index.js → examples/classic/chat/api/index.js


+ 0 - 0
examples/chat/api/mock-data.js → examples/classic/chat/api/mock-data.js


+ 13 - 0
examples/classic/chat/app.js

@@ -0,0 +1,13 @@
+import 'babel-polyfill'
+import { createApp } from 'vue'
+import App from './components/App.vue'
+import store from './store'
+import { getAllMessages } from './store/actions'
+
+const app = createApp(App)
+
+app.use(store)
+
+app.mount('#app')
+
+getAllMessages(store)

+ 0 - 0
examples/chat/components/App.vue → examples/classic/chat/components/App.vue


+ 6 - 1
examples/chat/components/Message.vue → examples/classic/chat/components/Message.vue

@@ -2,7 +2,7 @@
   <li class="message-list-item">
     <h5 class="message-author-name">{{ message.authorName }}</h5>
     <div class="message-time">
-      {{ message.timestamp | time }}
+      {{ time(message.timestamp) }}
     </div>
     <div class="message-text">{{ message.text }}</div>
   </li>
@@ -13,6 +13,11 @@ export default {
   name: 'Message',
   props: {
     message: Object
+  },
+  methods: {
+    time (value) {
+      return new Date(value).toLocaleTimeString()
+    }
   }
 }
 </script>

+ 0 - 0
examples/chat/components/MessageSection.vue → examples/classic/chat/components/MessageSection.vue


+ 7 - 2
examples/chat/components/Thread.vue → examples/classic/chat/components/Thread.vue

@@ -1,11 +1,11 @@
 <template>
   <li
     class="thread-list-item"
-    :class="{ active: active }"
+    :class="{ active }"
     @click="$emit('switch-thread', thread.id)">
     <h5 class="thread-name">{{ thread.name }}</h5>
     <div class="thread-time">
-      {{ thread.lastMessage.timestamp | time }}
+      {{ time(thread.lastMessage.timestamp) }}
     </div>
     <div class="thread-last-message">
       {{ thread.lastMessage.text }}
@@ -19,6 +19,11 @@ export default {
   props: {
     thread: Object,
     active: Boolean
+  },
+  methods: {
+    time (value) {
+      return new Date(value).toLocaleTimeString()
+    }
   }
 }
 </script>

+ 0 - 0
examples/chat/components/ThreadSection.vue → examples/classic/chat/components/ThreadSection.vue


+ 0 - 0
examples/chat/css/chat.css → examples/classic/chat/css/chat.css


+ 1 - 1
examples/chat/index.html → examples/classic/chat/index.html

@@ -8,6 +8,6 @@
   <body>
     <div id="app"></div>
     <script src="/__build__/shared.js"></script>
-    <script src="/__build__/chat.js"></script>
+    <script src="/__build__/classic/chat.js"></script>
   </body>
 </html>

+ 0 - 0
examples/chat/store/actions.js → examples/classic/chat/store/actions.js


+ 0 - 0
examples/chat/store/getters.js → examples/classic/chat/store/getters.js


+ 3 - 6
examples/chat/store/index.js → examples/classic/chat/store/index.js

@@ -1,11 +1,8 @@
-import Vue from 'vue'
-import Vuex from 'vuex'
+import { createStore } from 'vuex'
 import * as getters from './getters'
 import * as actions from './actions'
 import mutations from './mutations'
-import createLogger from '../../../src/plugins/logger'
-
-Vue.use(Vuex)
+import createLogger from '../../../../src/plugins/logger'
 
 const state = {
   currentThreadID: null,
@@ -34,7 +31,7 @@ const state = {
   }
 }
 
-export default new Vuex.Store({
+export default createStore({
   state,
   getters,
   actions,

+ 13 - 9
examples/chat/store/mutations.js → examples/classic/chat/store/mutations.js

@@ -1,5 +1,3 @@
-import Vue from 'vue'
-
 export default {
   receiveAll (state, messages) {
     let latestMessage
@@ -29,12 +27,15 @@ export default {
 }
 
 function createThread (state, id, name) {
-  Vue.set(state.threads, id, {
-    id,
-    name,
-    messages: [],
-    lastMessage: null
-  })
+  state.threads = {
+    ...state.threads,
+    [id]: {
+      id,
+      name,
+      messages: [],
+      lastMessage: null
+    }
+  }
 }
 
 function addMessage (state, message) {
@@ -47,7 +48,10 @@ function addMessage (state, message) {
     thread.lastMessage = message
   }
   // add it to the messages map
-  Vue.set(state.messages, message.id, message)
+  state.messages = {
+    ...state.messages,
+    [message.id]: message
+  }
 }
 
 function setCurrentThread (state, id) {

+ 0 - 0
examples/counter-hot/CounterControls.vue → examples/classic/counter-hot/CounterControls.vue


+ 9 - 0
examples/classic/counter-hot/app.js

@@ -0,0 +1,9 @@
+import { createApp } from 'vue'
+import store from './store'
+import CounterControls from './CounterControls.vue'
+
+const app = createApp(CounterControls)
+
+app.use(store)
+
+app.mount('#app')

+ 1 - 1
examples/counter/index.html → examples/classic/counter-hot/index.html

@@ -8,6 +8,6 @@
   <body>
     <div id="app"></div>
     <script src="/__build__/shared.js"></script>
-    <script src="/__build__/counter.js"></script>
+    <script src="/__build__/classic/counter-hot.js"></script>
   </body>
 </html>

+ 0 - 0
examples/counter-hot/store/actions.js → examples/classic/counter-hot/store/actions.js


+ 0 - 0
examples/counter-hot/store/getters.js → examples/classic/counter-hot/store/getters.js


+ 2 - 5
examples/counter-hot/store/index.js → examples/classic/counter-hot/store/index.js

@@ -1,17 +1,14 @@
-import Vue from 'vue'
-import Vuex from 'vuex'
+import { createStore } from 'vuex'
 import * as getters from './getters'
 import * as actions from './actions'
 import * as mutations from './mutations'
 
-Vue.use(Vuex)
-
 const state = {
   count: 0,
   history: []
 }
 
-const store = new Vuex.Store({
+const store = createStore({
   state,
   getters,
   actions,

+ 0 - 0
examples/counter-hot/store/mutations.js → examples/classic/counter-hot/store/mutations.js


+ 0 - 0
examples/counter/Counter.vue → examples/classic/counter/Counter.vue


+ 10 - 0
examples/classic/counter/app.js

@@ -0,0 +1,10 @@
+import 'babel-polyfill'
+import { createApp } from 'vue'
+import Counter from './Counter.vue'
+import store from './store'
+
+const app = createApp(Counter)
+
+app.use(store)
+
+app.mount('#app')

+ 1 - 1
examples/counter-hot/index.html → examples/classic/counter/index.html

@@ -8,6 +8,6 @@
   <body>
     <div id="app"></div>
     <script src="/__build__/shared.js"></script>
-    <script src="/__build__/counter-hot.js"></script>
+    <script src="/__build__/classic/counter.js"></script>
   </body>
 </html>

+ 2 - 5
examples/counter/store.js → examples/classic/counter/store.js

@@ -1,7 +1,4 @@
-import Vue from 'vue'
-import Vuex from 'vuex'
-
-Vue.use(Vuex)
+import { createStore } from 'vuex'
 
 // root state object.
 // each Vuex instance is just a single state tree.
@@ -50,7 +47,7 @@ const getters = {
 
 // A Vuex instance is created by combining the state, mutations, actions,
 // and getters.
-export default new Vuex.Store({
+export default createStore({
   state,
   getters,
   actions,

+ 0 - 0
examples/shopping-cart/api/shop.js → examples/classic/shopping-cart/api/shop.js


+ 7 - 7
examples/shopping-cart/app.js → examples/classic/shopping-cart/app.js

@@ -1,13 +1,13 @@
 import 'babel-polyfill'
-import Vue from 'vue'
+import { createApp } from 'vue'
 import App from './components/App.vue'
 import store from './store'
 import { currency } from './currency'
 
-Vue.filter('currency', currency)
+// Vue.filter('currency', currency)
 
-new Vue({
-  el: '#app',
-  store,
-  render: h => h(App)
-})
+const app = createApp(App)
+
+app.use(store)
+
+app.mount('#app')

+ 0 - 0
examples/shopping-cart/components/App.vue → examples/classic/shopping-cart/components/App.vue


+ 8 - 4
examples/shopping-cart/components/ProductList.vue → examples/classic/shopping-cart/components/ProductList.vue

@@ -3,7 +3,7 @@
     <li
       v-for="product in products"
       :key="product.id">
-      {{ product.title }} - {{ product.price | currency }}
+      {{ product.title }} - {{ currency(product.price) }}
       <br>
       <button
         :disabled="!product.inventory"
@@ -16,14 +16,18 @@
 
 <script>
 import { mapState, mapActions } from 'vuex'
+import { currency } from '../currency'
 
 export default {
   computed: mapState({
     products: state => state.products.all
   }),
-  methods: mapActions('cart', [
-    'addProductToCart'
-  ]),
+  methods: {
+    ...mapActions('cart', [
+      'addProductToCart'
+    ]),
+    currency
+  },
   created () {
     this.$store.dispatch('products/getAllProducts')
   }

+ 4 - 2
examples/shopping-cart/components/ShoppingCart.vue → examples/classic/shopping-cart/components/ShoppingCart.vue

@@ -6,10 +6,10 @@
       <li
         v-for="product in products"
         :key="product.id">
-        {{ product.title }} - {{ product.price | currency }} x {{ product.quantity }}
+        {{ product.title }} - {{ currency(product.price) }} x {{ product.quantity }}
       </li>
     </ul>
-    <p>Total: {{ total | currency }}</p>
+    <p>Total: {{ currency(total) }}</p>
     <p><button :disabled="!products.length" @click="checkout(products)">Checkout</button></p>
     <p v-show="checkoutStatus">Checkout {{ checkoutStatus }}.</p>
   </div>
@@ -17,6 +17,7 @@
 
 <script>
 import { mapGetters, mapState } from 'vuex'
+import { currency } from '../currency'
 
 export default {
   computed: {
@@ -29,6 +30,7 @@ export default {
     })
   },
   methods: {
+    currency,
     checkout (products) {
       this.$store.dispatch('cart/checkout', products)
     }

+ 0 - 0
examples/shopping-cart/currency.js → examples/classic/shopping-cart/currency.js


+ 1 - 1
examples/shopping-cart/index.html → examples/classic/shopping-cart/index.html

@@ -8,6 +8,6 @@
   <body>
     <div id="app"></div>
     <script src="/__build__/shared.js"></script>
-    <script src="/__build__/shopping-cart.js"></script>
+    <script src="/__build__/classic/shopping-cart.js"></script>
   </body>
 </html>

+ 3 - 6
examples/shopping-cart/store/index.js → examples/classic/shopping-cart/store/index.js

@@ -1,14 +1,11 @@
-import Vue from 'vue'
-import Vuex from 'vuex'
+import { createStore } from 'vuex'
 import cart from './modules/cart'
 import products from './modules/products'
-import createLogger from '../../../src/plugins/logger'
-
-Vue.use(Vuex)
+import createLogger from '../../../../src/plugins/logger'
 
 const debug = process.env.NODE_ENV !== 'production'
 
-export default new Vuex.Store({
+export default createStore({
   modules: {
     cart,
     products

+ 0 - 0
examples/shopping-cart/store/modules/cart.js → examples/classic/shopping-cart/store/modules/cart.js


+ 0 - 0
examples/shopping-cart/store/modules/products.js → examples/classic/shopping-cart/store/modules/products.js


+ 9 - 0
examples/classic/todomvc/app.js

@@ -0,0 +1,9 @@
+import { createApp } from 'vue'
+import store from './store'
+import App from './components/App.vue'
+
+const app = createApp(App)
+
+app.use(store)
+
+app.mount('#app')

+ 8 - 6
examples/todomvc/components/App.vue → examples/classic/todomvc/components/App.vue

@@ -30,13 +30,13 @@
     <footer class="footer" v-show="todos.length">
       <span class="todo-count">
         <strong>{{ remaining }}</strong>
-        {{ remaining | pluralize('item') }} left
+        {{ pluralize(remaining, 'item') }} left
       </span>
       <ul class="filters">
         <li v-for="(val, key) in filters">
           <a :href="'#/' + key"
             :class="{ selected: visibility === key }"
-            @click="visibility = key">{{ key | capitalize }}</a>
+            @click="visibility = key">{{ capitalize(key) }}</a>
         </li>
       </ul>
       <button class="clear-completed"
@@ -91,11 +91,13 @@ export default {
         this.$store.dispatch('addTodo', text)
       }
       e.target.value = ''
+    },
+    pluralize (n, w) {
+      return n === 1 ? w : (w + 's')
+    },
+    capitalize (s) {
+      return s.charAt(0).toUpperCase() + s.slice(1)
     }
-  },
-  filters: {
-    pluralize: (n, w) => n === 1 ? w : (w + 's'),
-    capitalize: s => s.charAt(0).toUpperCase() + s.slice(1)
   }
 }
 </script>

+ 8 - 2
examples/todomvc/components/TodoItem.vue → examples/classic/todomvc/components/TodoItem.vue

@@ -1,5 +1,5 @@
 <template>
-  <li class="todo" :class="{ completed: todo.done, editing: editing }">
+  <li class="todo" :class="{ completed: todo.done, editing }">
     <div class="view">
       <input class="toggle"
         type="checkbox"
@@ -10,8 +10,8 @@
     </div>
     <input class="edit"
       v-show="editing"
-      v-focus="editing"
       :value="todo.text"
+      ref="input"
       @keyup.enter="doneEdit"
       @keyup.esc="cancelEdit"
       @blur="doneEdit">
@@ -19,6 +19,7 @@
 </template>
 
 <script>
+import { nextTick } from 'vue'
 import { mapActions } from 'vuex'
 
 export default {
@@ -38,6 +39,11 @@ export default {
       }
     }
   },
+  watch: {
+    editing (v) {
+      v && nextTick(() => { this.$refs.input.focus() })
+    }
+  },
   methods: {
     ...mapActions([
       'editTodo',

+ 1 - 1
examples/todomvc/index.html → examples/classic/todomvc/index.html

@@ -7,6 +7,6 @@
   <body>
     <div id="app"></div>
     <script src="/__build__/shared.js"></script>
-    <script src="/__build__/todomvc.js"></script>
+    <script src="/__build__/classic/todomvc.js"></script>
   </body>
 </html>

+ 0 - 0
examples/todomvc/store/actions.js → examples/classic/todomvc/store/actions.js


+ 2 - 5
examples/todomvc/store/index.js → examples/classic/todomvc/store/index.js

@@ -1,12 +1,9 @@
-import Vue from 'vue'
-import Vuex from 'vuex'
+import { createStore } from 'vuex'
 import { mutations, STORAGE_KEY } from './mutations'
 import actions from './actions'
 import plugins from './plugins'
 
-Vue.use(Vuex)
-
-export default new Vuex.Store({
+export default createStore({
   state: {
     todos: JSON.parse(window.localStorage.getItem(STORAGE_KEY) || '[]')
   },

+ 1 - 1
examples/todomvc/store/mutations.js → examples/classic/todomvc/store/mutations.js

@@ -1,7 +1,7 @@
 export const STORAGE_KEY = 'todos-vuejs'
 
 // for testing
-if (navigator.userAgent.indexOf('PhantomJS') > -1) {
+if (navigator.webdriver) {
   window.localStorage.clear()
 }
 

+ 1 - 1
examples/todomvc/store/plugins.js → examples/classic/todomvc/store/plugins.js

@@ -1,5 +1,5 @@
 import { STORAGE_KEY } from './mutations'
-import createLogger from '../../../src/plugins/logger'
+import createLogger from '../../../../src/plugins/logger'
 
 const localStoragePlugin = store => {
   store.subscribe((mutation, { todos }) => {

+ 24 - 0
examples/composition/chat/api/index.js

@@ -0,0 +1,24 @@
+const data = require('./mock-data')
+const LATENCY = 16
+
+export function getAllMessages (cb) {
+  setTimeout(() => {
+    cb(data)
+  }, LATENCY)
+}
+
+export function createMessage ({ text, thread }, cb) {
+  const timestamp = Date.now()
+  const id = 'm_' + timestamp
+  const message = {
+    id,
+    text,
+    timestamp,
+    threadID: thread.id,
+    threadName: thread.name,
+    authorName: 'Evan'
+  }
+  setTimeout(function () {
+    cb(message)
+  }, LATENCY)
+}

+ 58 - 0
examples/composition/chat/api/mock-data.js

@@ -0,0 +1,58 @@
+module.exports = [
+  {
+    id: 'm_1',
+    threadID: 't_1',
+    threadName: 'Jing and Bill',
+    authorName: 'Bill',
+    text: 'Hey Jing, want to give a Flux talk at ForwardJS?',
+    timestamp: Date.now() - 99999
+  },
+  {
+    id: 'm_2',
+    threadID: 't_1',
+    threadName: 'Jing and Bill',
+    authorName: 'Bill',
+    text: 'Seems like a pretty cool conference.',
+    timestamp: Date.now() - 89999
+  },
+  {
+    id: 'm_3',
+    threadID: 't_1',
+    threadName: 'Jing and Bill',
+    authorName: 'Jing',
+    text: 'Sounds good.  Will they be serving dessert?',
+    timestamp: Date.now() - 79999
+  },
+  {
+    id: 'm_4',
+    threadID: 't_2',
+    threadName: 'Dave and Bill',
+    authorName: 'Bill',
+    text: 'Hey Dave, want to get a beer after the conference?',
+    timestamp: Date.now() - 69999
+  },
+  {
+    id: 'm_5',
+    threadID: 't_2',
+    threadName: 'Dave and Bill',
+    authorName: 'Dave',
+    text: 'Totally!  Meet you at the hotel bar.',
+    timestamp: Date.now() - 59999
+  },
+  {
+    id: 'm_6',
+    threadID: 't_3',
+    threadName: 'Functional Heads',
+    authorName: 'Bill',
+    text: 'Hey Brian, are you going to be talking about functional stuff?',
+    timestamp: Date.now() - 49999
+  },
+  {
+    id: 'm_7',
+    threadID: 't_3',
+    threadName: 'Bill and Brian',
+    authorName: 'Brian',
+    text: 'At ForwardJS?  Yeah, of course.  See you there!',
+    timestamp: Date.now() - 39999
+  }
+]

+ 13 - 0
examples/composition/chat/app.js

@@ -0,0 +1,13 @@
+import 'babel-polyfill'
+import { createApp } from 'vue'
+import App from './components/App.vue'
+import store from './store'
+import { getAllMessages } from './store/actions'
+
+const app = createApp(App)
+
+app.use(store)
+
+app.mount('#app')
+
+getAllMessages(store)

+ 21 - 0
examples/composition/chat/components/App.vue

@@ -0,0 +1,21 @@
+<style src="../css/chat.css"></style>
+
+<template>
+  <div class="chatapp">
+    <thread-section></thread-section>
+    <message-section></message-section>
+  </div>
+</template>
+
+<script>
+import ThreadSection from './ThreadSection.vue'
+import MessageSection from './MessageSection.vue'
+
+export default {
+  name: 'App',
+  components: {
+    ThreadSection,
+    MessageSection
+  }
+}
+</script>

+ 23 - 0
examples/composition/chat/components/Message.vue

@@ -0,0 +1,23 @@
+<template>
+  <li class="message-list-item">
+    <h5 class="message-author-name">{{ message.authorName }}</h5>
+    <div class="message-time">
+      {{ time(message.timestamp) }}
+    </div>
+    <div class="message-text">{{ message.text }}</div>
+  </li>
+</template>
+
+<script>
+export default {
+  name: 'Message',
+  props: {
+    message: Object
+  },
+  setup () {
+    return {
+      time: value => new Date(value).toLocaleTimeString()
+    }
+  }
+}
+</script>

+ 63 - 0
examples/composition/chat/components/MessageSection.vue

@@ -0,0 +1,63 @@
+<template>
+  <div class="message-section">
+    <h3 class="message-thread-heading">{{ thread.name }}</h3>
+    <ul class="message-list" ref="list">
+      <message
+        v-for="message in messages"
+        :key="message.id"
+        :message="message">
+      </message>
+    </ul>
+    <textarea
+      class="message-composer"
+      v-model="text"
+      @keyup.enter="sendMessage"></textarea>
+  </div>
+</template>
+
+<script>
+import { ref, computed, watch, nextTick } from 'vue'
+import { useStore } from 'vuex'
+import Message from './Message.vue'
+
+export default {
+  name: 'MessageSection',
+  components: { Message },
+  setup () {
+    const list = ref(null)
+
+    const store = useStore()
+
+    const text = ref('')
+
+    const thread = computed(() => store.getters.currentThread)
+    const messages = computed(() => store.getters.sortedMessages)
+
+    watch(() => thread.value.lastMessage, () => {
+      nextTick(() => {
+        const ul = list.value
+        ul.scrollTop = ul.scrollHeight
+      })
+    })
+
+    function sendMessage () {
+      const trimedText = text.value.trim()
+      if (trimedText) {
+        store.dispatch('sendMessage', {
+          text: trimedText,
+          thread: thread.value
+        })
+        this.text = ''
+      }
+    }
+
+    return {
+      list,
+      text,
+      thread,
+      messages,
+      sendMessage
+    }
+  }
+}
+</script>

+ 29 - 0
examples/composition/chat/components/Thread.vue

@@ -0,0 +1,29 @@
+<template>
+  <li
+    class="thread-list-item"
+    :class="{ active }"
+    @click="$emit('switch-thread', thread.id)">
+    <h5 class="thread-name">{{ thread.name }}</h5>
+    <div class="thread-time">
+      {{ time(thread.lastMessage.timestamp) }}
+    </div>
+    <div class="thread-last-message">
+      {{ thread.lastMessage.text }}
+    </div>
+  </li>
+</template>
+
+<script>
+export default {
+  name: 'Thread',
+  props: {
+    thread: Object,
+    active: Boolean
+  },
+  setup () {
+    return {
+      time: value => new Date(value).toLocaleTimeString()
+    }
+  }
+}
+</script>

+ 39 - 0
examples/composition/chat/components/ThreadSection.vue

@@ -0,0 +1,39 @@
+<template>
+  <div class="thread-section">
+    <div class="thread-count">
+      <span v-show="unreadCount">
+        Unread threads: {{ unreadCount }}
+      </span>
+    </div>
+    <ul class="thread-list">
+      <thread
+        v-for="thread in threads"
+        :key="thread.id"
+        :thread="thread"
+        :active="thread.id === currentThread.id"
+        @switch-thread="switchThread">
+      </thread>
+    </ul>
+  </div>
+</template>
+
+<script>
+import { computed } from 'vue'
+import { useStore } from 'vuex'
+import Thread from './Thread.vue'
+
+export default {
+  name: 'ThreadSection',
+  components: { Thread },
+  setup () {
+    const store = useStore()
+
+    return {
+      threads: computed(() => store.getters.threads),
+      currentThread: computed(() => store.getters.currentThread),
+      unreadCount: computed(() => store.getters.unreadCount),
+      switchThread: (id) => store.dispatch('switchThread', id)
+    }
+  }
+}
+</script>

+ 98 - 0
examples/composition/chat/css/chat.css

@@ -0,0 +1,98 @@
+/**
+ * This file is provided by Facebook for testing and evaluation purposes
+ * only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+.chatapp {
+  font-family: 'Muli', 'Helvetica Neue', helvetica, arial;
+  max-width: 760px;
+  margin: 20px auto;
+  overflow: hidden;
+}
+
+.message-list, .thread-list {
+  border: 1px solid #ccf;
+  font-size: 16px;
+  height: 400px;
+  margin: 0;
+  overflow-y: auto;
+  padding: 0;
+}
+
+.message-section {
+  float: right;
+  width: 65%;
+}
+
+.thread-section {
+  float: left;
+  width: 32.5%;
+}
+
+.message-thread-heading,
+.thread-count {
+  height: 40px;
+  margin: 0;
+}
+
+.message-list-item, .thread-list-item {
+  list-style: none;
+  padding: 12px 14px 14px;
+}
+
+.thread-list-item {
+  border-bottom: 1px solid #ccc;
+  cursor: pointer;
+}
+
+.thread-list:hover .thread-list-item:hover {
+  background-color: #f8f8ff;
+}
+
+.thread-list:hover .thread-list-item {
+  background-color: #fff;
+}
+
+.thread-list-item.active,
+.thread-list:hover .thread-list-item.active,
+.thread-list:hover .thread-list-item.active:hover {
+  background-color: #efefff;
+  cursor: default;
+}
+
+.message-author-name,
+.thread-name {
+  color: #66c;
+  float: left;
+  font-size: 13px;
+  margin: 0;
+}
+
+.message-time, .thread-time {
+  color: #aad;
+  float: right;
+  font-size: 12px;
+}
+
+.message-text, .thread-last-message {
+  clear: both;
+  font-size: 14px;
+  padding-top: 10px;
+}
+
+.message-composer {
+  box-sizing: border-box;
+  font-family: inherit;
+  font-size: 14px;
+  height: 5em;
+  width: 100%;
+  margin: 20px 0 0;
+  padding: 10px;
+}

+ 13 - 0
examples/composition/chat/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>vuex chat example</title>
+    <link href="http://fonts.googleapis.com/css?family=Muli" rel="stylesheet" type="text/css">
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="/__build__/shared.js"></script>
+    <script src="/__build__/composition/chat.js"></script>
+  </body>
+</html>

+ 17 - 0
examples/composition/chat/store/actions.js

@@ -0,0 +1,17 @@
+import * as api from '../api'
+
+export const getAllMessages = ({ commit }) => {
+  api.getAllMessages(messages => {
+    commit('receiveAll', messages)
+  })
+}
+
+export const sendMessage = ({ commit }, payload) => {
+  api.createMessage(payload, message => {
+    commit('receiveMessage', message)
+  })
+}
+
+export const switchThread = ({ commit }, payload) => {
+  commit('switchThread', payload)
+}

+ 25 - 0
examples/composition/chat/store/getters.js

@@ -0,0 +1,25 @@
+export const threads = state => state.threads
+
+export const currentThread = state => {
+  return state.currentThreadID
+    ? state.threads[state.currentThreadID]
+    : {}
+}
+
+export const currentMessages = state => {
+  const thread = currentThread(state)
+  return thread.messages
+    ? thread.messages.map(id => state.messages[id])
+    : []
+}
+
+export const unreadCount = ({ threads }) => {
+  return Object.keys(threads).reduce((count, id) => {
+    return threads[id].lastMessage.isRead ? count : count + 1
+  }, 0)
+}
+
+export const sortedMessages = (state, getters) => {
+  const messages = getters.currentMessages
+  return messages.slice().sort((a, b) => a.timestamp - b.timestamp)
+}

+ 42 - 0
examples/composition/chat/store/index.js

@@ -0,0 +1,42 @@
+import { createStore } from 'vuex'
+import * as getters from './getters'
+import * as actions from './actions'
+import mutations from './mutations'
+import createLogger from '../../../../src/plugins/logger'
+
+const state = {
+  currentThreadID: null,
+  threads: {
+    /*
+    id: {
+      id,
+      name,
+      messages: [...ids],
+      lastMessage
+    }
+    */
+  },
+  messages: {
+    /*
+    id: {
+      id,
+      threadId,
+      threadName,
+      authorName,
+      text,
+      timestamp,
+      isRead
+    }
+    */
+  }
+}
+
+export default createStore({
+  state,
+  getters,
+  actions,
+  mutations,
+  plugins: process.env.NODE_ENV !== 'production'
+    ? [createLogger()]
+    : []
+})

+ 64 - 0
examples/composition/chat/store/mutations.js

@@ -0,0 +1,64 @@
+export default {
+  receiveAll (state, messages) {
+    let latestMessage
+    messages.forEach(message => {
+      // create new thread if the thread doesn't exist
+      if (!state.threads[message.threadID]) {
+        createThread(state, message.threadID, message.threadName)
+      }
+      // mark the latest message
+      if (!latestMessage || message.timestamp > latestMessage.timestamp) {
+        latestMessage = message
+      }
+      // add message
+      addMessage(state, message)
+    })
+    // set initial thread to the one with the latest message
+    setCurrentThread(state, latestMessage.threadID)
+  },
+
+  receiveMessage (state, message) {
+    addMessage(state, message)
+  },
+
+  switchThread (state, id) {
+    setCurrentThread(state, id)
+  }
+}
+
+function createThread (state, id, name) {
+  state.threads = {
+    ...state.threads,
+    [id]: {
+      id,
+      name,
+      messages: [],
+      lastMessage: null
+    }
+  }
+}
+
+function addMessage (state, message) {
+  // add a `isRead` field before adding the message
+  message.isRead = message.threadID === state.currentThreadID
+  // add it to the thread it belongs to
+  const thread = state.threads[message.threadID]
+  if (!thread.messages.some(id => id === message.id)) {
+    thread.messages.push(message.id)
+    thread.lastMessage = message
+  }
+  // add it to the messages map
+  state.messages = {
+    ...state.messages,
+    [message.id]: message
+  }
+}
+
+function setCurrentThread (state, id) {
+  state.currentThreadID = id
+  if (!state.threads[id]) {
+    debugger
+  }
+  // mark thread as read
+  state.threads[id].lastMessage.isRead = true
+}

+ 32 - 0
examples/composition/counter-hot/CounterControls.vue

@@ -0,0 +1,32 @@
+<template>
+  <div>
+    Value: {{ count }}
+    <button @click="increment">+</button>
+    <button @click="decrement">-</button>
+    <button @click="incrementIfOdd">Increment if odd</button>
+    <button @click="incrementAsync">Increment async</button>
+    <div>
+      <div>Recent History (last 5 entries): {{ recentHistory }}</div>
+    </div>
+  </div>
+</template>
+
+<script>
+import { computed } from 'vue'
+import { useStore } from 'vuex'
+
+export default {
+  setup () {
+    const store = useStore()
+
+    return {
+      count: computed(() => store.getters.count),
+      recentHistory: computed(() => store.getters.recentHistory),
+      increment: () => store.dispatch('increment'),
+      decrement: () => store.dispatch('decrement'),
+      incrementIfOdd: () => store.dispatch('incrementIfOdd'),
+      incrementAsync: () => store.dispatch('incrementAsync')
+    }
+  }
+}
+</script>

+ 9 - 0
examples/composition/counter-hot/app.js

@@ -0,0 +1,9 @@
+import { createApp } from 'vue'
+import store from './store'
+import CounterControls from './CounterControls.vue'
+
+const app = createApp(CounterControls)
+
+app.use(store)
+
+app.mount('#app')

+ 13 - 0
examples/composition/counter-hot/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>vuex counter example</title>
+    <link rel="stylesheet" href="/global.css">
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="/__build__/shared.js"></script>
+    <script src="/__build__/composition/counter-hot.js"></script>
+  </body>
+</html>

+ 18 - 0
examples/composition/counter-hot/store/actions.js

@@ -0,0 +1,18 @@
+export const increment = ({ commit }) => {
+  commit('increment')
+}
+export const decrement = ({ commit }) => {
+  commit('decrement')
+}
+
+export const incrementIfOdd = ({ commit, state }) => {
+  if ((state.count + 1) % 2 === 0) {
+    commit('increment')
+  }
+}
+
+export const incrementAsync = ({ commit }) => {
+  setTimeout(() => {
+    commit('increment')
+  }, 1000)
+}

+ 15 - 0
examples/composition/counter-hot/store/getters.js

@@ -0,0 +1,15 @@
+export const count = state => state.count
+
+export const t = (state) => {
+  return state.test
+}
+
+const limit = 5
+
+export const recentHistory = state => {
+  const end = state.history.length
+  const begin = end - limit < 0 ? 0 : end - limit
+  return state.history
+    .slice(begin, end)
+    .join(', ')
+}

+ 33 - 0
examples/composition/counter-hot/store/index.js

@@ -0,0 +1,33 @@
+import { createStore } from 'vuex'
+import * as getters from './getters'
+import * as actions from './actions'
+import * as mutations from './mutations'
+
+const state = {
+  test: 0,
+  count: 0,
+  history: []
+}
+
+const store = createStore({
+  state,
+  getters,
+  actions,
+  mutations
+})
+
+if (module.hot) {
+  module.hot.accept([
+    './getters',
+    './actions',
+    './mutations'
+  ], () => {
+    store.hotUpdate({
+      getters: require('./getters'),
+      actions: require('./actions'),
+      mutations: require('./mutations')
+    })
+  })
+}
+
+export default store

+ 9 - 0
examples/composition/counter-hot/store/mutations.js

@@ -0,0 +1,9 @@
+export const increment = state => {
+  state.count++
+  state.history.push('increment')
+}
+
+export const decrement = state => {
+  state.count--
+  state.history.push('decrement')
+}

+ 35 - 0
examples/composition/counter/Counter.vue

@@ -0,0 +1,35 @@
+<template>
+  <div id="app">
+    Clicked: {{ count }} times, count is {{ evenOrOdd }}.
+    <button @click="increment">+</button>
+    <button @click="decrement">-</button>
+    <button @click="incrementIfOdd">Increment if odd</button>
+    <button @click="incrementAsync">Increment async</button>
+  </div>
+</template>
+
+<script>
+import { computed } from 'vue'
+import { useStore } from 'vuex'
+
+export default {
+  setup () {
+    const store = useStore()
+
+    return {
+      count: computed(() => store.state.count),
+      evenOrOdd: computed(() => store.getters.evenOrOdd),
+      // increment: () => store.dispatch('increment'),
+      decrement: () => store.dispatch('decrement'),
+      incrementIfOdd: () => store.dispatch('incrementIfOdd'),
+      incrementAsync: () => store.dispatch('incrementAsync')
+    }
+  },
+
+  methods: {
+    increment () {
+      this.$store.dispatch('increment')
+    }
+  }
+}
+</script>

+ 10 - 0
examples/composition/counter/app.js

@@ -0,0 +1,10 @@
+import 'babel-polyfill'
+import { createApp } from 'vue'
+import Counter from './Counter.vue'
+import store from './store'
+
+const app = createApp(Counter)
+
+app.use(store)
+
+app.mount('#app')

+ 13 - 0
examples/composition/counter/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>vuex counter example</title>
+    <link rel="stylesheet" href="/global.css">
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="/__build__/shared.js"></script>
+    <script src="/__build__/composition/counter.js"></script>
+  </body>
+</html>

+ 55 - 0
examples/composition/counter/store.js

@@ -0,0 +1,55 @@
+import { createStore } from 'vuex'
+
+// root state object.
+// each Vuex instance is just a single state tree.
+const state = {
+  count: 0
+}
+
+// mutations are operations that actually mutate the state.
+// each mutation handler gets the entire state tree as the
+// first argument, followed by additional payload arguments.
+// mutations must be synchronous and can be recorded by plugins
+// for debugging purposes.
+const mutations = {
+  increment (state) {
+    state.count++
+  },
+  decrement (state) {
+    state.count--
+  }
+}
+
+// actions are functions that cause side effects and can involve
+// asynchronous operations.
+const actions = {
+  increment: ({ commit }) => commit('increment'),
+  decrement: ({ commit }) => commit('decrement'),
+  incrementIfOdd ({ commit, state }) {
+    if ((state.count + 1) % 2 === 0) {
+      commit('increment')
+    }
+  },
+  incrementAsync ({ commit }) {
+    return new Promise((resolve, reject) => {
+      setTimeout(() => {
+        commit('increment')
+        resolve()
+      }, 1000)
+    })
+  }
+}
+
+// getters are functions.
+const getters = {
+  evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
+}
+
+// A Vuex instance is created by combining the state, mutations, actions,
+// and getters.
+export default createStore({
+  state,
+  getters,
+  actions,
+  mutations
+})

+ 23 - 0
examples/composition/shopping-cart/api/shop.js

@@ -0,0 +1,23 @@
+/**
+ * Mocking client-server processing
+ */
+const _products = [
+  {"id": 1, "title": "iPad 4 Mini", "price": 500.01, "inventory": 2},
+  {"id": 2, "title": "H&M T-Shirt White", "price": 10.99, "inventory": 10},
+  {"id": 3, "title": "Charli XCX - Sucker CD", "price": 19.99, "inventory": 5}
+]
+
+export default {
+  getProducts (cb) {
+    setTimeout(() => cb(_products), 100)
+  },
+
+  buyProducts (products, cb, errorCb) {
+    setTimeout(() => {
+      // simulate random checkout failure.
+      (Math.random() > 0.5 || navigator.webdriver)
+        ? cb()
+        : errorCb()
+    }, 100)
+  }
+}

+ 13 - 0
examples/composition/shopping-cart/app.js

@@ -0,0 +1,13 @@
+import 'babel-polyfill'
+import { createApp } from 'vue'
+import App from './components/App.vue'
+import store from './store'
+import { currency } from './currency'
+
+// Vue.filter('currency', currency)
+
+const app = createApp(App)
+
+app.use(store)
+
+app.mount('#app')

+ 19 - 0
examples/composition/shopping-cart/components/App.vue

@@ -0,0 +1,19 @@
+<template>
+  <div id="app">
+    <h1>Shopping Cart Example</h1>
+    <hr>
+    <h2>Products</h2>
+    <ProductList/>
+    <hr>
+    <ShoppingCart/>
+  </div>
+</template>
+
+<script>
+import ProductList from './ProductList.vue'
+import ShoppingCart from './ShoppingCart.vue'
+
+export default {
+  components: { ProductList, ShoppingCart }
+}
+</script>

+ 48 - 0
examples/composition/shopping-cart/components/ProductList.vue

@@ -0,0 +1,48 @@
+<template>
+  <ul>
+    <li
+      v-for="product in products"
+      :key="product.id">
+      {{ product.title }} - {{ currency(product.price) }}
+      <br>
+      <button
+        :disabled="!product.inventory"
+        @click="addProductToCart(product)">
+        Add to cart
+      </button>
+    </li>
+  </ul>
+</template>
+
+<script>
+import { computed } from 'vue'
+import { useStore } from 'vuex'
+import { currency } from '../currency'
+
+export default {
+  setup () {
+    const store = useStore()
+
+    const products = computed(() => store.state.products.all)
+
+    const addProductToCart = (product) => store.dispatch('cart/addProductToCart', product)
+
+    store.dispatch('products/getAllProducts')
+
+    return {
+      products,
+      addProductToCart,
+      currency
+    }
+  }
+  // computed: mapState({
+  //   products: state => state.products.all
+  // }),
+  // methods: mapActions('cart', [
+  //   'addProductToCart'
+  // ]),
+  // created () {
+  //   this.$store.dispatch('products/getAllProducts')
+  // }
+}
+</script>

+ 42 - 0
examples/composition/shopping-cart/components/ShoppingCart.vue

@@ -0,0 +1,42 @@
+<template>
+  <div class="cart">
+    <h2>Your Cart</h2>
+    <p v-show="!products.length"><i>Please add some products to cart.</i></p>
+    <ul>
+      <li
+        v-for="product in products"
+        :key="product.id">
+        {{ product.title }} - {{ currency(product.price) }} x {{ product.quantity }}
+      </li>
+    </ul>
+    <p>Total: {{ currency(total) }}</p>
+    <p><button :disabled="!products.length" @click="checkout(products)">Checkout</button></p>
+    <p v-show="checkoutStatus">Checkout {{ checkoutStatus }}.</p>
+  </div>
+</template>
+
+<script>
+import { computed } from 'vue'
+import { useStore } from 'vuex'
+import { currency } from '../currency'
+
+export default {
+  setup () {
+    const store = useStore()
+
+    const checkoutStatus = computed(() => store.state.cart.checkoutStatus)
+    const products = computed(() => store.getters['cart/cartProducts'])
+    const total = computed(() => store.getters['cart/cartTotalPrice'])
+
+    const checkout = (products) => store.dispatch('cart/checkout', products)
+
+    return {
+      currency,
+      checkoutStatus,
+      products,
+      total,
+      checkout
+    }
+  }
+}
+</script>

+ 23 - 0
examples/composition/shopping-cart/currency.js

@@ -0,0 +1,23 @@
+const digitsRE = /(\d{3})(?=\d)/g
+
+export function currency (value, currency, decimals) {
+  value = parseFloat(value)
+  if (!isFinite(value) || (!value && value !== 0)) return ''
+  currency = currency != null ? currency : '$'
+  decimals = decimals != null ? decimals : 2
+  var stringified = Math.abs(value).toFixed(decimals)
+  var _int = decimals
+    ? stringified.slice(0, -1 - decimals)
+    : stringified
+  var i = _int.length % 3
+  var head = i > 0
+    ? (_int.slice(0, i) + (_int.length > 3 ? ',' : ''))
+    : ''
+  var _float = decimals
+    ? stringified.slice(-1 - decimals)
+    : ''
+  var sign = value < 0 ? '-' : ''
+  return sign + currency + head +
+    _int.slice(i).replace(digitsRE, '$1,') +
+    _float
+}

+ 13 - 0
examples/composition/shopping-cart/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>vuex shopping cart example</title>
+    <link rel="stylesheet" href="/global.css">
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="/__build__/shared.js"></script>
+    <script src="/__build__/composition/shopping-cart.js"></script>
+  </body>
+</html>

+ 15 - 0
examples/composition/shopping-cart/store/index.js

@@ -0,0 +1,15 @@
+import { createStore } from 'vuex'
+import cart from './modules/cart'
+import products from './modules/products'
+import createLogger from '../../../../src/plugins/logger'
+
+const debug = process.env.NODE_ENV !== 'production'
+
+export default createStore({
+  modules: {
+    cart,
+    products
+  },
+  strict: debug,
+  plugins: debug ? [createLogger()] : []
+})

+ 92 - 0
examples/composition/shopping-cart/store/modules/cart.js

@@ -0,0 +1,92 @@
+import shop from '../../api/shop'
+
+// initial state
+// shape: [{ id, quantity }]
+const state = {
+  items: [],
+  checkoutStatus: null
+}
+
+// getters
+const getters = {
+  cartProducts: (state, getters, rootState) => {
+    return state.items.map(({ id, quantity }) => {
+      const product = rootState.products.all.find(product => product.id === id)
+      return {
+        title: product.title,
+        price: product.price,
+        quantity
+      }
+    })
+  },
+
+  cartTotalPrice: (state, getters) => {
+    return getters.cartProducts.reduce((total, product) => {
+      return total + product.price * product.quantity
+    }, 0)
+  }
+}
+
+// actions
+const actions = {
+  checkout ({ commit, state }, products) {
+    const savedCartItems = [...state.items]
+    commit('setCheckoutStatus', null)
+    // empty cart
+    commit('setCartItems', { items: [] })
+    shop.buyProducts(
+      products,
+      () => commit('setCheckoutStatus', 'successful'),
+      () => {
+        commit('setCheckoutStatus', 'failed')
+        // rollback to the cart saved before sending the request
+        commit('setCartItems', { items: savedCartItems })
+      }
+    )
+  },
+
+  addProductToCart ({ state, commit }, product) {
+    commit('setCheckoutStatus', null)
+    if (product.inventory > 0) {
+      const cartItem = state.items.find(item => item.id === product.id)
+      if (!cartItem) {
+        commit('pushProductToCart', { id: product.id })
+      } else {
+        commit('incrementItemQuantity', cartItem)
+      }
+      // remove 1 item from stock
+      commit('products/decrementProductInventory', { id: product.id }, { root: true })
+    }
+  }
+}
+
+// mutations
+const mutations = {
+  pushProductToCart (state, { id }) {
+    state.items.push({
+      id,
+      quantity: 1
+    })
+  },
+
+  incrementItemQuantity (state, { id }) {
+    const cartItem = state.items.find(item => item.id === id)
+    cartItem.quantity++
+  },
+
+  setCartItems (state, { items }) {
+    state.items = items
+  },
+
+  setCheckoutStatus (state, status) {
+    state.checkoutStatus = status
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  getters,
+  actions,
+  mutations
+}

+ 38 - 0
examples/composition/shopping-cart/store/modules/products.js

@@ -0,0 +1,38 @@
+import shop from '../../api/shop'
+
+// initial state
+const state = {
+  all: []
+}
+
+// getters
+const getters = {}
+
+// actions
+const actions = {
+  getAllProducts ({ commit }) {
+    shop.getProducts(products => {
+      commit('setProducts', products)
+    })
+  }
+}
+
+// mutations
+const mutations = {
+  setProducts (state, products) {
+    state.all = products
+  },
+
+  decrementProductInventory (state, { id }) {
+    const product = state.all.find(product => product.id === id)
+    product.inventory--
+  }
+}
+
+export default {
+  namespaced: true,
+  state,
+  getters,
+  actions,
+  mutations
+}

+ 9 - 0
examples/composition/todomvc/app.js

@@ -0,0 +1,9 @@
+import { createApp } from 'vue'
+import store from './store'
+import App from './components/App.vue'
+
+const app = createApp(App)
+
+app.use(store)
+
+app.mount('#app')

+ 103 - 0
examples/composition/todomvc/components/App.vue

@@ -0,0 +1,103 @@
+<style src="todomvc-app-css/index.css"></style>
+
+<template>
+  <section class="todoapp">
+    <!-- header -->
+    <header class="header">
+      <h1>todos</h1>
+      <input class="new-todo"
+        autofocus
+        autocomplete="off"
+        placeholder="What needs to be done?"
+        @keyup.enter="addTodo">
+    </header>
+    <!-- main section -->
+    <section class="main" v-show="todos.length">
+      <input class="toggle-all" id="toggle-all"
+        type="checkbox"
+        :checked="allChecked"
+        @change="toggleAll(!allChecked)">
+      <label for="toggle-all"></label>
+      <ul class="todo-list">
+        <TodoItem
+          v-for="(todo, index) in filteredTodos"
+          :key="index"
+          :todo="todo"
+        />
+      </ul>
+    </section>
+    <!-- footer -->
+    <footer class="footer" v-show="todos.length">
+      <span class="todo-count">
+        <strong>{{ remaining }}</strong>
+        {{ pluralize(remaining, 'item') }} left
+      </span>
+      <ul class="filters">
+        <li v-for="(val, key) in filters">
+          <a :href="'#/' + key"
+            :class="{ selected: visibility === key }"
+            @click="visibility = key">{{ capitalize(key) }}</a>
+        </li>
+      </ul>
+      <button class="clear-completed"
+        v-show="todos.length > remaining"
+        @click="clearCompleted">
+        Clear completed
+      </button>
+    </footer>
+  </section>
+</template>
+
+<script>
+import { ref, computed } from 'vue'
+import { useStore } from 'vuex'
+import TodoItem from './TodoItem.vue'
+
+const filters = {
+  all: todos => todos,
+  active: todos => todos.filter(todo => !todo.done),
+  completed: todos => todos.filter(todo => todo.done)
+}
+
+export default {
+  components: { TodoItem },
+  setup () {
+    const visibility = ref('all')
+
+    const store = useStore()
+
+    const todos = computed(() => store.state.todos)
+    const allChecked = computed(() => todos.value.every(todo => todo.done))
+    const filteredTodos = computed(() => filters[visibility.value](todos.value))
+    const remaining = computed(() => todos.value.filter(todo => !todo.done).length)
+
+    const toggleAll = (done) => store.dispatch('toggleAll', done)
+    const clearCompleted = () => store.dispatch('clearCompleted')
+
+    function addTodo (e) {
+      const text = e.target.value
+      if (text.trim()) {
+        store.dispatch('addTodo', text)
+      }
+      e.target.value = ''
+    }
+
+    const pluralize = (n, w) => n === 1 ? w : (w + 's')
+    const capitalize = s => s.charAt(0).toUpperCase() + s.slice(1)
+
+    return {
+      visibility,
+      filters,
+      todos,
+      allChecked,
+      filteredTodos,
+      remaining,
+      addTodo,
+      clearCompleted,
+      toggleAll,
+      pluralize,
+      capitalize
+    }
+  }
+}
+</script>

+ 68 - 0
examples/composition/todomvc/components/TodoItem.vue

@@ -0,0 +1,68 @@
+<template>
+  <li class="todo" :class="{ completed: todo.done, editing }">
+    <div class="view">
+      <input class="toggle"
+        type="checkbox"
+        :checked="todo.done"
+        @change="toggleTodo(todo)">
+      <label v-text="todo.text" @dblclick="editing = true"></label>
+      <button class="destroy" @click="removeTodo(todo)"></button>
+    </div>
+    <input class="edit"
+      v-show="editing"
+      :value="todo.text"
+      ref="input"
+      @keyup.enter="doneEdit"
+      @keyup.esc="cancelEdit"
+      @blur="doneEdit">
+  </li>
+</template>
+
+<script>
+import { ref, watch, nextTick } from 'vue'
+import { useStore } from 'vuex'
+
+export default {
+  name: 'Todo',
+  props: ['todo'],
+  setup (props) {
+    const input = ref(null)
+
+    const editing = ref(false)
+
+    watch(editing, (v) => {
+      v && nextTick(() => { input.value.focus() })
+    })
+
+    const store = useStore()
+
+    const editTodo = (todo, value) => store.dispatch('editTodo', { todo, value })
+    const toggleTodo = (todo) => store.dispatch('toggleTodo', todo)
+    const removeTodo = (todo) => store.dispatch('removeTodo', todo)
+
+    function doneEdit (e) {
+      const value = e.target.value.trim()
+      if (!value) {
+        removeTodo(props.todo)
+      } else if (editing.value) {
+        editTodo(props.todo, value)
+      }
+      editing.value = false
+    }
+
+    function cancelEdit (e) {
+      e.target.value = props.todo.text
+      editing.value = false
+    }
+
+    return {
+      input,
+      editing,
+      toggleTodo,
+      doneEdit,
+      cancelEdit,
+      removeTodo
+    }
+  }
+}
+</script>

+ 12 - 0
examples/composition/todomvc/index.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>vuex todomvc example</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script src="/__build__/shared.js"></script>
+    <script src="/__build__/composition/todomvc.js"></script>
+  </body>
+</html>

+ 33 - 0
examples/composition/todomvc/store/actions.js

@@ -0,0 +1,33 @@
+export default {
+  addTodo ({ commit }, text) {
+    commit('addTodo', {
+      text,
+      done: false
+    })
+  },
+
+  removeTodo ({ commit }, todo) {
+    commit('removeTodo', todo)
+  },
+
+  toggleTodo ({ commit }, todo) {
+    commit('editTodo', { todo, done: !todo.done })
+  },
+
+  editTodo ({ commit }, { todo, value }) {
+    commit('editTodo', { todo, text: value })
+  },
+
+  toggleAll ({ state, commit }, done) {
+    state.todos.forEach((todo) => {
+      commit('editTodo', { todo, done })
+    })
+  },
+
+  clearCompleted ({ state, commit }) {
+    state.todos.filter(todo => todo.done)
+      .forEach(todo => {
+        commit('removeTodo', todo)
+      })
+  }
+}

+ 13 - 0
examples/composition/todomvc/store/index.js

@@ -0,0 +1,13 @@
+import { createStore } from 'vuex'
+import { mutations, STORAGE_KEY } from './mutations'
+import actions from './actions'
+import plugins from './plugins'
+
+export default createStore({
+  state: {
+    todos: JSON.parse(window.localStorage.getItem(STORAGE_KEY) || '[]')
+  },
+  actions,
+  mutations,
+  plugins
+})

+ 26 - 0
examples/composition/todomvc/store/mutations.js

@@ -0,0 +1,26 @@
+export const STORAGE_KEY = 'todos-vuejs'
+
+// for testing
+if (navigator.webdriver) {
+  window.localStorage.clear()
+}
+
+export const mutations = {
+  addTodo (state, todo) {
+    state.todos.push(todo)
+  },
+
+  removeTodo (state, todo) {
+    state.todos.splice(state.todos.indexOf(todo), 1)
+  },
+
+  editTodo (state, { todo, text = todo.text, done = todo.done }) {
+    const index = state.todos.indexOf(todo)
+
+    state.todos.splice(index, 1, {
+      ...todo,
+      text,
+      done
+    })
+  }
+}

+ 12 - 0
examples/composition/todomvc/store/plugins.js

@@ -0,0 +1,12 @@
+import { STORAGE_KEY } from './mutations'
+import createLogger from '../../../../src/plugins/logger'
+
+const localStoragePlugin = store => {
+  store.subscribe((mutation, { todos }) => {
+    window.localStorage.setItem(STORAGE_KEY, JSON.stringify(todos))
+  })
+}
+
+export default process.env.NODE_ENV !== 'production'
+  ? [createLogger(), localStoragePlugin]
+  : [localStoragePlugin]

+ 0 - 9
examples/counter-hot/app.js

@@ -1,9 +0,0 @@
-import Vue from 'vue'
-import store from './store'
-import CounterControls from './CounterControls.vue'
-
-new Vue({
-  el: '#app',
-  store,
-  render: h => h(CounterControls)
-})

+ 0 - 10
examples/counter/app.js

@@ -1,10 +0,0 @@
-import 'babel-polyfill'
-import Vue from 'vue'
-import Counter from './Counter.vue'
-import store from './store'
-
-new Vue({
-  el: '#app',
-  store,
-  render: h => h(Counter)
-})

+ 18 - 5
examples/index.html

@@ -7,12 +7,25 @@
   </head>
   <body style="padding: 0 20px">
     <h1>Vuex Examples</h1>
+
+    <h2>Classic API</h2>
+
     <ul>
-      <li><a href="counter">Counter</a></li>
-      <li><a href="counter-hot">Counter with Hot Reload</a></li>
-      <li><a href="shopping-cart">Shopping Cart</a></li>
-      <li><a href="todomvc">TodoMVC</a></li>
-      <li><a href="chat">FluxChat</a></li>
+      <li><a href="classic/counter">Counter</a></li>
+      <li><a href="classic/counter-hot">Counter with Hot Reload</a></li>
+      <li><a href="classic/shopping-cart">Shopping Cart</a></li>
+      <li><a href="classic/todomvc">TodoMVC</a></li>
+      <li><a href="classic/chat">FluxChat</a></li>
+    </ul>
+
+    <h2>Composition API</h2>
+
+    <ul>
+      <li><a href="composition/counter">Counter</a></li>
+      <li><a href="composition/counter-hot">Counter with Hot Reload</a></li>
+      <li><a href="composition/shopping-cart">Shopping Cart</a></li>
+      <li><a href="composition/todomvc">TodoMVC</a></li>
+      <li><a href="composition/chat">FluxChat</a></li>
     </ul>
   </body>
 </html>

+ 4 - 0
examples/server.js

@@ -19,6 +19,10 @@ app.use(webpackHotMiddleware(compiler))
 
 app.use(express.static(__dirname))
 
+app.use((req, res, next) => {
+  res.redirect('/')
+})
+
 const port = process.env.PORT || 8080
 module.exports = app.listen(port, () => {
   console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`)

+ 0 - 10
examples/todomvc/app.js

@@ -1,10 +0,0 @@
-import 'babel-polyfill'
-import Vue from 'vue'
-import store from './store'
-import App from './components/App.vue'
-
-new Vue({
-  store, // inject store to all children
-  el: '#app',
-  render: h => h(App)
-})

+ 16 - 7
examples/webpack.config.js

@@ -1,20 +1,29 @@
 const fs = require('fs')
 const path = require('path')
 const webpack = require('webpack')
-const VueLoaderPlugin = require('vue-loader/lib/plugin')
+const { VueLoaderPlugin } = require('vue-loader')
 
-module.exports = {
-  mode: 'development',
+function buildEntry (dirname) {
+  const lookupDir = path.join(__dirname, dirname)
 
-  entry: fs.readdirSync(__dirname).reduce((entries, dir) => {
-    const fullDir = path.join(__dirname, dir)
+  return fs.readdirSync(lookupDir).reduce((entries, dir) => {
+    const fullDir = path.join(lookupDir, dir)
     const entry = path.join(fullDir, 'app.js')
     if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
-      entries[dir] = ['webpack-hot-middleware/client', entry]
+      entries[`${dirname}/${dir}`] = ['webpack-hot-middleware/client', entry]
     }
 
     return entries
-  }, {}),
+  }, {})
+}
+
+module.exports = {
+  mode: 'development',
+
+  entry: {
+    ...buildEntry('classic'),
+    ...buildEntry('composition')
+  },
 
   output: {
     path: path.join(__dirname, '__build__'),

+ 6 - 5
package.json

@@ -1,6 +1,6 @@
 {
   "name": "vuex",
-  "version": "3.1.3",
+  "version": "4.0.0-alpha.1",
   "description": "state management for Vue.js",
   "main": "dist/vuex.common.js",
   "module": "dist/vuex.esm.js",
@@ -20,7 +20,7 @@
     "build:main": "node build/build.main.js",
     "build:logger": "rollup -c build/rollup.logger.config.js",
     "lint": "eslint src test",
-    "test": "npm run lint && npm run test:types && npm run test:unit && npm run test:ssr && npm run test:e2e",
+    "test": "npm run lint && npm run test:unit && npm run test:ssr && npm run test:e2e",
     "test:unit": "rollup -c build/rollup.dev.config.js && jasmine JASMINE_CONFIG_PATH=test/unit/jasmine.json",
     "test:e2e": "node test/e2e/runner.js",
     "test:ssr": "rollup -c build/rollup.dev.config.js && cross-env VUE_ENV=server jasmine JASMINE_CONFIG_PATH=test/unit/jasmine.json",
@@ -40,9 +40,10 @@
   },
   "homepage": "https://github.com/vuejs/vuex#readme",
   "peerDependencies": {
-    "vue": "^2.0.0"
+    "vue": "^3.0.0-alpha.8"
   },
   "devDependencies": {
+    "@vue/compiler-sfc": "3.0.0-alpha.8",
     "babel-core": "^6.22.1",
     "babel-loader": "^7.1.2",
     "babel-plugin-transform-object-rest-spread": "^6.23.0",
@@ -65,8 +66,8 @@
     "terser": "^3.17.0",
     "todomvc-app-css": "^2.1.0",
     "typescript": "^3.7.2",
-    "vue": "^2.5.22",
-    "vue-loader": "^15.2.1",
+    "vue": "3.0.0-alpha.8",
+    "vue-loader": "16.0.0-alpha.3",
     "vue-template-compiler": "^2.5.22",
     "vuepress": "^0.14.1",
     "vuepress-theme-vue": "^1.1.0",

+ 7 - 4
src/index.esm.js

@@ -1,10 +1,12 @@
-import { Store, install } from './store'
+import { createStore, Store } from './store'
+import { useStore } from './injectKey'
 import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
 
 export default {
-  Store,
-  install,
   version: '__VERSION__',
+  createStore,
+  Store,
+  useStore,
   mapState,
   mapMutations,
   mapGetters,
@@ -13,8 +15,9 @@ export default {
 }
 
 export {
+  createStore,
   Store,
-  install,
+  useStore,
   mapState,
   mapMutations,
   mapGetters,

+ 5 - 3
src/index.js

@@ -1,10 +1,12 @@
-import { Store, install } from './store'
+import { createStore, Store } from './store'
+import { useStore } from './injectKey'
 import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
 
 export default {
-  Store,
-  install,
   version: '__VERSION__',
+  createStore,
+  Store,
+  useStore,
   mapState,
   mapMutations,
   mapGetters,

+ 7 - 0
src/injectKey.js

@@ -0,0 +1,7 @@
+import { inject } from 'vue'
+
+export const storeKey = 'store'
+
+export function useStore (key = null) {
+  return inject(key !== null ? key : storeKey)
+}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません