From 76ead8847c17002a38fc5c34a65a93f1ab337da0 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 28 Nov 2015 01:14:42 -0500 Subject: [PATCH] init --- .eslintrc | 177 +++++++++++++++++++++++++++ .gitignore | 3 + README.md | 8 ++ examples/todomvc/components/App.vue | 96 +++++++++++++++ examples/todomvc/components/Todo.vue | 63 ++++++++++ examples/todomvc/index.html | 11 ++ examples/todomvc/main.js | 7 ++ examples/todomvc/vuex/actions.js | 8 ++ examples/todomvc/vuex/index.js | 19 +++ examples/todomvc/vuex/middlewares.js | 7 ++ examples/todomvc/vuex/mutations.js | 30 +++++ examples/todomvc/webpack.config.js | 24 ++++ package.json | 41 +++++++ src/cursor.js | 38 ++++++ src/index.js | 114 +++++++++++++++++ src/mixin.js | 47 +++++++ webpack.config.js | 0 17 files changed, 693 insertions(+) create mode 100644 .eslintrc create mode 100644 .gitignore create mode 100644 README.md create mode 100644 examples/todomvc/components/App.vue create mode 100644 examples/todomvc/components/Todo.vue create mode 100644 examples/todomvc/index.html create mode 100644 examples/todomvc/main.js create mode 100644 examples/todomvc/vuex/actions.js create mode 100644 examples/todomvc/vuex/index.js create mode 100644 examples/todomvc/vuex/middlewares.js create mode 100644 examples/todomvc/vuex/mutations.js create mode 100644 examples/todomvc/webpack.config.js create mode 100644 package.json create mode 100644 src/cursor.js create mode 100644 src/index.js create mode 100644 src/mixin.js create mode 100644 webpack.config.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..be8961c2e --- /dev/null +++ b/.eslintrc @@ -0,0 +1,177 @@ +{ + "env": { + "browser": true, + "node": true + }, + + "ecmaFeatures": { + "arrowFunctions": true, + "destructuring": true, + "classes": true, + "defaultParams": true, + "blockBindings": true, + "modules": true, + "objectLiteralComputedProperties": true, + "objectLiteralShorthandMethods": true, + "objectLiteralShorthandProperties": true, + "restParams": true, + "spread": true, + "templateStrings": true + }, + + "rules": { + "accessor-pairs": 2, + "array-bracket-spacing": 0, + "block-scoped-var": 0, + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "camelcase": 0, + "comma-dangle": [2, "never"], + "comma-spacing": [2, { "before": false, "after": true }], + "comma-style": [2, "last"], + "complexity": 0, + "computed-property-spacing": 0, + "consistent-return": 0, + "consistent-this": 0, + "constructor-super": 2, + "curly": [2, "multi-line"], + "default-case": 0, + "dot-location": [2, "property"], + "dot-notation": 0, + "eol-last": 2, + "eqeqeq": [2, "allow-null"], + "func-names": 0, + "func-style": 0, + "generator-star-spacing": [2, { "before": true, "after": true }], + "guard-for-in": 0, + "handle-callback-err": [2, "^(err|error)$" ], + "indent": [2, 2, { "SwitchCase": 1 }], + "key-spacing": [2, { "beforeColon": false, "afterColon": true }], + "linebreak-style": 0, + "lines-around-comment": 0, + "max-nested-callbacks": 0, + "new-cap": [2, { "newIsCap": true, "capIsNew": false }], + "new-parens": 2, + "newline-after-var": 0, + "no-alert": 0, + "no-array-constructor": 2, + "no-caller": 2, + "no-catch-shadow": 0, + "no-cond-assign": 2, + "no-console": 0, + "no-constant-condition": 0, + "no-continue": 0, + "no-control-regex": 2, + "no-debugger": 2, + "no-delete-var": 2, + "no-div-regex": 0, + "no-dupe-args": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-else-return": 0, + "no-empty": 0, + "no-empty-character-class": 2, + "no-empty-label": 2, + "no-eq-null": 0, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": 0, + "no-extra-semi": 0, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + "no-implied-eval": 2, + "no-inline-comments": 0, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-lonely-if": 0, + "no-loop-func": 0, + "no-mixed-requires": 0, + "no-mixed-spaces-and-tabs": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [2, { "max": 1 }], + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-nested-ternary": 0, + "no-new": 2, + "no-new-func": 0, + "no-new-object": 2, + "no-new-require": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-param-reassign": 0, + "no-path-concat": 0, + "no-process-env": 0, + "no-process-exit": 0, + "no-proto": 0, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-restricted-modules": 0, + "no-return-assign": 2, + "no-script-url": 0, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow": 0, + "no-shadow-restricted-names": 2, + "no-spaced-func": 2, + "no-sparse-arrays": 2, + "no-sync": 0, + "no-ternary": 0, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-undefined": 0, + "no-underscore-dangle": 0, + "no-unexpected-multiline": 2, + "no-unneeded-ternary": 2, + "no-unreachable": 2, + "no-unused-expressions": 0, + "no-unused-vars": [2, { "vars": "all", "args": "none" }], + "no-use-before-define": 0, + "no-var": 0, + "no-void": 0, + "no-warning-comments": 0, + "no-with": 2, + "object-curly-spacing": 0, + "object-shorthand": 0, + "one-var": [2, { "initialized": "never" }], + "operator-assignment": 0, + "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], + "padded-blocks": 0, + "prefer-const": 0, + "quote-props": 0, + "quotes": [2, "single", "avoid-escape"], + "radix": 2, + "semi": [2, "never"], + "semi-spacing": 0, + "sort-vars": 0, + "space-after-keywords": [2, "always"], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "always"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-return-throw-case": 2, + "space-unary-ops": [2, { "words": true, "nonwords": false }], + "spaced-comment": [2, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!"] }], + "strict": 0, + "use-isnan": 2, + "valid-jsdoc": 0, + "valid-typeof": 2, + "vars-on-top": 0, + "wrap-iife": [2, "any"], + "wrap-regex": 0, + "yoda": [2, "never"] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..325eb43f4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +node_modules +TODO.md diff --git a/README.md b/README.md new file mode 100644 index 000000000..0783fc779 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +## Principles + +- Terse +- Testable +- Reactive +- Single State Tree +- Hot Reloading +- Time Travel diff --git a/examples/todomvc/components/App.vue b/examples/todomvc/components/App.vue new file mode 100644 index 000000000..470076838 --- /dev/null +++ b/examples/todomvc/components/App.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/examples/todomvc/components/Todo.vue b/examples/todomvc/components/Todo.vue new file mode 100644 index 000000000..4408a3ed7 --- /dev/null +++ b/examples/todomvc/components/Todo.vue @@ -0,0 +1,63 @@ + + + diff --git a/examples/todomvc/index.html b/examples/todomvc/index.html new file mode 100644 index 000000000..4a8d60b74 --- /dev/null +++ b/examples/todomvc/index.html @@ -0,0 +1,11 @@ + + + + + vue store example + + + + + + diff --git a/examples/todomvc/main.js b/examples/todomvc/main.js new file mode 100644 index 000000000..9e600ebdf --- /dev/null +++ b/examples/todomvc/main.js @@ -0,0 +1,7 @@ +import Vue from 'vue' +import App from './components/App.vue' + +new Vue({ + el: 'body', + components: { App } +}) diff --git a/examples/todomvc/vuex/actions.js b/examples/todomvc/vuex/actions.js new file mode 100644 index 000000000..847a7d41c --- /dev/null +++ b/examples/todomvc/vuex/actions.js @@ -0,0 +1,8 @@ +export default { + addTodo: 'ADD_TODO', + deleteTodo: 'DELETE_TODO', + toggleTodo: 'TOGGLE_TODO', + editTodo: 'EDIT_TODO', + toggleAll: 'TOGGLE_ALL', + clearCompleted: 'CLEAR_COMPLETED' +} diff --git a/examples/todomvc/vuex/index.js b/examples/todomvc/vuex/index.js new file mode 100644 index 000000000..4ca0a6920 --- /dev/null +++ b/examples/todomvc/vuex/index.js @@ -0,0 +1,19 @@ +import Vue from 'vue' +import Vuex from '../../../src' +import actions from './actions' +import mutations from './mutations' +import middlewares from './middlewares' + +Vue.use(Vuex) + +export const STORAGE_KEY = 'todos-vuejs' +const state = { + todos: JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]') +} + +export default new Vuex({ + state, + actions, + mutations, + middlewares +}) diff --git a/examples/todomvc/vuex/middlewares.js b/examples/todomvc/vuex/middlewares.js new file mode 100644 index 000000000..0c8dde0cd --- /dev/null +++ b/examples/todomvc/vuex/middlewares.js @@ -0,0 +1,7 @@ +import { STORAGE_KEY } from './index' + +export default [ + function (action, state) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state.todos)) + } +] diff --git a/examples/todomvc/vuex/mutations.js b/examples/todomvc/vuex/mutations.js new file mode 100644 index 000000000..5277f61cf --- /dev/null +++ b/examples/todomvc/vuex/mutations.js @@ -0,0 +1,30 @@ +export default { + ADD_TODO (state, text) { + state.todos.unshift({ + text: text, + done: false + }) + }, + + DELETE_TODO (state, todo) { + state.todos.$remove(todo) + }, + + TOGGLE_TODO (state, todo) { + todo.done = !todo.done + }, + + EDIT_TODO (state, todo, text) { + todo.text = text + }, + + TOGGLE_ALL (state, done) { + state.todos.forEach((todo) => { + todo.done = done + }) + }, + + CLEAR_COMPLETED (state) { + state.todos = state.todos.filter(todo => !todo.done) + } +} diff --git a/examples/todomvc/webpack.config.js b/examples/todomvc/webpack.config.js new file mode 100644 index 000000000..e9f5deda7 --- /dev/null +++ b/examples/todomvc/webpack.config.js @@ -0,0 +1,24 @@ +module.exports = { + entry: './main.js', + output: { + path: __dirname, + filename: 'example.build.js' + }, + module: { + loaders: [ + { + test: /\.js$/, + loader: 'babel', + exclude: /node_modules|vue\/dist|vue-hot-reload-api|vue-loader/ + }, + { + test: /\.vue$/, + loader: 'vue' + } + ] + }, + babel: { + presets: ['es2015'] + }, + devtool: 'source-map' +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..6af8bd90f --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "vuex", + "version": "1.0.0", + "description": "state management for Vue.js", + "main": "src/index.js", + "scripts": { + "dev": "cd examples/todomvc && webpack-dev-server --inline --hot" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vuejs/vue-store.git" + }, + "keywords": [ + "vuejs", + "state", + "vue", + "store" + ], + "author": "Evan You", + "license": "MIT", + "bugs": { + "url": "https://github.com/vuejs/vue-store/issues" + }, + "homepage": "https://github.com/vuejs/vue-store#readme", + "devDependencies": { + "babel-core": "^6.2.1", + "babel-loader": "^6.2.0", + "babel-plugin-transform-runtime": "^6.1.18", + "babel-preset-es2015": "^6.1.18", + "babel-runtime": "^6.2.0", + "css-loader": "^0.21.0", + "style-loader": "^0.13.0", + "todomvc-app-css": "^2.0.3", + "vue": "^1.0.8", + "vue-hot-reload-api": "^1.2.1", + "vue-html-loader": "^1.0.0", + "vue-loader": "^7.1.1", + "webpack": "^1.12.8", + "webpack-dev-server": "^1.12.1" + } +} diff --git a/src/cursor.js b/src/cursor.js new file mode 100644 index 000000000..2bb80c146 --- /dev/null +++ b/src/cursor.js @@ -0,0 +1,38 @@ +export default class Cursor { + + /** + * @param {Vue} vm + * @param {String} path + */ + + constructor (vm, path) { + this.cb = null + this.vm = vm + this.path = path + this.dispose = vm.$watch(path, value => { + if (this.cb) { + this.cb.call(null, value) + } + }) + } + + /** + * Get the latest value. + * + * @return {*} + */ + + get () { + return this.vm.$get(this.path) + } + + /** + * Set the subscribe callback. + * + * @param {Function} cb + */ + + subscribe (cb) { + this.cb = cb + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 000000000..583c288c9 --- /dev/null +++ b/src/index.js @@ -0,0 +1,114 @@ +import mixin from './mixin' +import Cursor from './cursor' + +let Vue + +export default class Vuex { + + /** + * @param {Object} options + * - {Object} state + * - {Object} actions + * - {Object} mutations + * - {Array} middlewares + */ + + constructor ({ + state = {}, + actions = {}, + mutations = {}, + middlewares = [] + } = {}) { + + // use a Vue instance to store the state tree + this._vm = new Vue({ + data: state + }) + + // create actions + this.actions = Object.create(null) + Object.keys(actions).forEach(name => { + this.actions[name] = createAction(actions[name], this) + }) + + // mutations + this._mutations = mutations + // middlewares + this._middlewares = middlewares + } + + /** + * "Get" the store's state, or a part of it. + * Returns a Cursor, which can be subscribed to for change, + * and disposed of when no longer needed. + * + * @param {String} [path] + * @return {Cursor} + */ + + get (path) { + return new Cursor(this._vm, path) + } + + /** + * Dispatch an action. + * + * @param {String} type + */ + + dispatch (type, ...payload) { + const mutation = this._mutations[type] + if (mutation) { + mutation(this.state, ...payload) + this._middlewares.forEach(middleware => { + middleware({ type, payload }, this.state) + }) + } else { + console.warn(`[vuex] Unknown mutation: ${ type }`) + } + } + + /** + * Getter for the entire state tree. + * + * @return {Object} + */ + + get state () { + return this._vm._data + } +} + +/** + * Exposed install method + */ + +Vuex.install = function (_Vue) { + Vue = _Vue + Vue.mixin(mixin) +} + +/** + * Create a actual callable action function. + * + * @param {String|Function} action + * @param {Vuex} vuex + * @return {Function} [description] + */ + +function createAction (action, vuex) { + if (typeof action === 'string') { + // simple action string shorthand + return (...payload) => { + vuex.dispatch(action, ...payload) + } + } else if (typeof action === 'function') { + // thunk action + return (...args) => { + const dispatch = (...args) => { + vuex.dispatch(...args) + } + action(...args)(dispatch, vuex.state) + } + } +} diff --git a/src/mixin.js b/src/mixin.js new file mode 100644 index 000000000..2ec0ba106 --- /dev/null +++ b/src/mixin.js @@ -0,0 +1,47 @@ +import Cursor from './cursor' + +export default { + + /** + * Patch the instance's data function so that we can + * directly bind to cursors in the `data` option. + */ + + init () { + const dataFn = this.$options.data + if (dataFn) { + this.$options.data = () => { + const raw = dataFn() + Object.keys(raw).forEach(key => { + const val = raw[key] + if (val instanceof Cursor) { + raw[key] = val.get() + if (val.cb) { + throw new Error( + '[vue-store] A vue-store can only be subscribed to once.' + ) + } + val.subscribe(value => { + this[key] = value + }) + if (!this._vue_store_cursors) { + this._vue_store_cursors = [] + } + this._vue_store_cursors.push(val) + } + }) + return raw + } + } + }, + + /** + * Dispose cursors owned by this instance. + */ + + beforeDestroy () { + if (this._vue_store_cursors) { + this._vue_store_cursors.forEach(c => c.dispose()) + } + } +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 000000000..e69de29bb