Evan You 9 years ago
commit
76ead8847c

+ 177 - 0
.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"]
+  }
+}

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+.DS_Store
+node_modules
+TODO.md

+ 8 - 0
README.md

@@ -0,0 +1,8 @@
+## Principles
+
+- Terse
+- Testable
+- Reactive
+- Single State Tree
+- Hot Reloading
+- Time Travel

+ 96 - 0
examples/todomvc/components/App.vue

@@ -0,0 +1,96 @@
+<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"
+        type="checkbox"
+        :checked="allChecked"
+        @change="toggleAll(!allChecked)">
+      <ul class="todo-list">
+        <todo v-for="todo in filteredTodos" :todo="todo"></todo>
+      </ul>
+    </section>
+    <!-- footer -->
+    <footer class="footer" v-show="todos.length">
+      <span class="todo-count">
+        <strong>{{ remaining }}</strong>
+        {{ remaining | pluralize 'item' }} left
+      </span>
+      <ul class="filters">
+        <li v-for="(key, val) in filters">
+          <a href="#/{{$key}}"
+            :class="{ selected: visibility === key }"
+            @click="visibility = key">
+            {{ key | capitalize }}
+          </a>
+        </li>
+      </ul>
+      <button class="clear-completed"
+        v-show="todos.length > remaining"
+        @click="clearCompleted">
+        Clear completed
+      </button>
+    </footer>
+  </section>
+</template>
+
+<script>
+import vuex from '../vuex'
+import Todo from './Todo.vue'
+
+const {
+  addTodo,
+  toggleAll,
+  clearCompleted
+} = vuex.actions
+
+const filters = {
+  all: (todos) => todos,
+  active: (todos) => todos.filter(todo => !todo.done),
+  completed: (todos) => todos.filter(todo => todo.done)
+}
+
+export default {
+  components: { Todo },
+  data () {
+    return {
+      todos: vuex.get('todos'),
+      visibility: 'all',
+      filters: filters
+    }
+  },
+  computed: {
+    allChecked () {
+      return this.todos.every(todo => todo.done)
+    },
+    filteredTodos () {
+      return filters[this.visibility](this.todos)
+    },
+    remaining () {
+      return this.todos.filter(todo => !todo.done).length
+    }
+  },
+  methods: {
+    addTodo (e) {
+      var text = e.target.value
+      if (text.trim()) {
+        addTodo(text)
+      }
+      e.target.value = ''
+    },
+    toggleAll,
+    clearCompleted
+  }
+}
+</script>

+ 63 - 0
examples/todomvc/components/Todo.vue

@@ -0,0 +1,63 @@
+<template>
+  <li class="todo" :class="{ completed: todo.done, editing: 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="deleteTodo(todo)"></button>
+    </div>
+    <input class="edit"
+      v-show="editing"
+      v-focus="editing"
+      :value="todo.text"
+      @keyup.enter="doneEdit"
+      @keyup.esc="cancelEdit"
+      @blur="doneEdit">
+  </li>
+</template>
+
+<script>
+import vuex from '../vuex'
+const {
+  toggleTodo,
+  deleteTodo,
+  editTodo
+} = vuex.actions
+
+export default {
+  props: ['todo'],
+  data () {
+    return {
+      editing: false
+    }
+  },
+  directives: {
+    focus (value) {
+      if (value) {
+        this.vm.$nextTick(() => {
+          this.el.focus()
+        })
+      }
+    }
+  },
+  methods: {
+    toggleTodo,
+    deleteTodo,
+    doneEdit (e) {
+      var value = e.target.value.trim()
+      if (!value) {
+        deleteTodo(this.todo)
+      } else if (this.editing) {
+        editTodo(this.todo, value)
+        this.editing = false
+      }
+    },
+    cancelEdit (e) {
+      e.target.value = this.todo.text
+      this.editing = false
+    }
+  }
+}
+</script>

+ 11 - 0
examples/todomvc/index.html

@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <title>vue store example</title>
+  </head>
+  <body>
+    <app></app>
+    <script src="example.build.js"></script>
+  </body>
+</html>

+ 7 - 0
examples/todomvc/main.js

@@ -0,0 +1,7 @@
+import Vue from 'vue'
+import App from './components/App.vue'
+
+new Vue({
+  el: 'body',
+  components: { App }
+})

+ 8 - 0
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'
+}

+ 19 - 0
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
+})

+ 7 - 0
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))
+  }
+]

+ 30 - 0
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)
+  }
+}

+ 24 - 0
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'
+}

+ 41 - 0
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"
+  }
+}

+ 38 - 0
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
+  }
+}

+ 114 - 0
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)
+    }
+  }
+}

+ 47 - 0
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())
+    }
+  }
+}

+ 0 - 0
webpack.config.js