diff --git a/README.md b/README.md index 230cd3c4d..9b8be7563 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - [Documentation](http://vuejs.github.io/vuex/) - [Great introduction and explanation by @skyronic](http://skyronic.com/2016/01/03/vuex-basics-tutorial/) (using outdated 0.3.0 API, but still worth a read!) - [Vuex introduction video - James Browne from London Vue.js Meetup #1](https://www.youtube.com/watch?v=l1KHL-TX3qs) +- [Feathers.js example using Vue.js and Vuex on the client to maintain state](https://github.com/niallobrien/feathers-chat-example) by [Niall O'Brien](https://github.com/niallobrien) (Twitter: [@niall_obrien](https://twitter.com/niall_obrien)) ## Examples diff --git a/docs/en/actions.md b/docs/en/actions.md index dc9efd402..3bb36bfb3 100644 --- a/docs/en/actions.md +++ b/docs/en/actions.md @@ -10,7 +10,7 @@ function increment (store) { store.dispatch('INCREMENT') } -// a action with additional arguments +// an action with additional arguments // with ES2015 argument destructuring function incrementBy ({ dispatch }, amount) { dispatch('INCREMENT', amount) diff --git a/docs/en/testing.md b/docs/en/testing.md index 318cbcc55..b4aae115d 100644 --- a/docs/en/testing.md +++ b/docs/en/testing.md @@ -164,4 +164,4 @@ mocha test-bundle.js #### Running in Browser with Karma + karma-webpack -Consult the setup in [vue-loader documentation](http://vuejs.github.io/vue-loader/workflow/testing.html). +Consult the setup in [vue-loader documentation](http://vue-loader.vuejs.org/en/workflow/testing.html). diff --git a/docs/en/tutorial.md b/docs/en/tutorial.md index 1e474a3f5..46c23727d 100644 --- a/docs/en/tutorial.md +++ b/docs/en/tutorial.md @@ -250,7 +250,7 @@ You might be wondering - why did we choose to use a getter instead of directly a 1. We may want to define getters with computed values (think totals, averages, etc.). 2. Many components in a larger app can use the same getter function. -3. If the value is moved from say `store.count` to `store.counter.value` you'd have to update one getter instead of dozens of components. +3. If the value is moved from say `store.state.count` to `store.state.counter.value` you'd have to update one getter instead of dozens of components. These are a few of the benefits of using getters. diff --git a/docs/ja/state.md b/docs/ja/state.md index 2d3c1edc7..1dd58c789 100644 --- a/docs/ja/state.md +++ b/docs/ja/state.md @@ -13,7 +13,7 @@ Vuex は**単一ステートツリー (single state tree)**を使います。つ ``` js // Vue コンポーネントの定義 computed: { -count: function() { + count: function() { return store.state.count } } @@ -67,7 +67,7 @@ count: function() { 特殊な `vuex` オプションブロックについて説明しておきましょう。これは、コンポーネントで利用されるストアの状態が何か記述する場所です。各プロパテイ名は、唯一の引数として、全体のステートツリーを受け取るゲッター関数を指定します。そして、状態の一部を選ぶか、状態から算出される値を返します。返された値は、算出プロパティのように、プロパティ名を使ってコンポーネントにセットされます。 - ほとんどの場合で、"ゲッター( getter )" 関数は、ES2015 のアロー関数を使って、完結に書くことができるでしょう: + ほとんどの場合で、"ゲッター( getter )" 関数は、ES2015 のアロー関数を使って、簡潔に書くことができるでしょう: ``` js vuex: { diff --git a/docs/ja/tutorial.md b/docs/ja/tutorial.md index 73c70b5c4..58e545d4e 100644 --- a/docs/ja/tutorial.md +++ b/docs/ja/tutorial.md @@ -145,7 +145,7 @@ export default { アクションはコンポーネントから呼び出される関数です。アクション関数は対応するミューテーションをディスパッチすることで、ストアを更新することができます。アクションは更新をディスパッチする前に、その他のデータを読み込むために HTTP バックエンドと通信することも可能です。 -`incrementCounter` 関数を持つ、新しいファイル `vuex/action.js` を作成しましょう。 +`incrementCounter` 関数を持つ、新しいファイル `vuex/actions.js` を作成しましょう。 ```js // アクションは1番目の引数にストアを受け取ります。 diff --git a/docs/zh-cn/mutations.md b/docs/zh-cn/mutations.md index d29dbd11d..47df4df01 100644 --- a/docs/zh-cn/mutations.md +++ b/docs/zh-cn/mutations.md @@ -99,7 +99,6 @@ import { SOME_MUTATION } from './mutation-types' const store = new Vuex.Store({ state: { ... }, - actions: { ... }, mutations: { // we can use the ES2015 computed property name feature // to use a constant as the function name diff --git a/package.json b/package.json index 69199db01..594923d6f 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "chat": "cd examples/chat && webpack-dev-server --inline --hot --config ../webpack.shared.config.js", "build": "node build/build.js", "build-examples": "BABEL_ENV=development webpack --config examples/webpack.build-all.config.js", - "unit": "BABEL_ENV=development mocha test/unit/test.js --compilers js:babel-core/register", + "unit": "BABEL_ENV=development mocha test/unit/*.js --compilers js:babel-core/register", "pree2e": "npm run build-examples", "e2e": "casperjs test --concise ./test/e2e", "test": "eslint src && npm run unit && npm run e2e", @@ -47,7 +47,7 @@ "babel-preset-es2015-rollup": "^1.1.1", "babel-preset-stage-2": "^6.1.18", "babel-runtime": "^6.0.0", - "casperjs": "^1.1.0-beta5", + "casperjs": "^1.1.3", "chai": "^3.4.1", "css-loader": "^0.23.1", "eslint": "^2.2.0", diff --git a/src/index.js b/src/index.js index 80600b06f..91945b904 100644 --- a/src/index.js +++ b/src/index.js @@ -261,11 +261,11 @@ class Store { } /** - * Setup mutation check: if the vuex instance's state is mutated + * Setup mutation check: if the Vuex instance's state is mutated * outside of a mutation handler, we throw en error. This effectively - * enforces all mutations to the state to be trackable and hot-reloadble. + * enforces all mutations to the state to be trackable and hot-reloadable. * However, this comes at a run time cost since we are doing a deep - * watch on the entire state tree, so it is only enalbed with the + * watch on the entire state tree, so it is only enabled if the * strict option is set to true. */ diff --git a/src/plugins/logger.js b/src/plugins/logger.js index 583620837..1bef55cc7 100644 --- a/src/plugins/logger.js +++ b/src/plugins/logger.js @@ -1,18 +1,20 @@ // Credits: borrowed code from fcomb/redux-logger +import { deepCopy } from '../util' + export default function createLogger ({ collapsed = true, transformer = state => state, mutationTransformer = mut => mut } = {}) { return store => { - let prevState = JSON.parse(JSON.stringify(store.state)) + let prevState = deepCopy(store.state) store.subscribe((mutation, state) => { if (typeof console === 'undefined') { return } - const nextState = JSON.parse(JSON.stringify(state)) + const nextState = deepCopy(state) const time = new Date() const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}` const formattedMutation = mutationTransformer(mutation) @@ -28,9 +30,9 @@ export default function createLogger ({ console.log(message) } - console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', prevState) + console.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState)) console.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation) - console.log('%c next state', 'color: #4CAF50; font-weight: bold', nextState) + console.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState)) try { console.groupEnd() diff --git a/src/util.js b/src/util.js index fea7d5e46..c8729791a 100644 --- a/src/util.js +++ b/src/util.js @@ -25,6 +25,54 @@ export function mergeObjects (arr) { }, {}) } +/** + * Get the first item that pass the test + * by second argument function + * + * @param {Array} list + * @param {Function} f + * @return {*} + */ +function find (list, f) { + return list.filter(f)[0] +} + +/** + * Deep copy the given object considering circular structure. + * This function caches all nested objects and its copies. + * If it detects circular structure, use cached copy to avoid infinite loop. + * + * @param {*} obj + * @param {Array} cache + * @return {*} + */ +export function deepCopy (obj, cache = []) { + // just return if obj is immutable value + if (obj === null || typeof obj !== 'object') { + return obj + } + + // if obj is hit, it is in circular structure + const hit = find(cache, c => c.original === obj) + if (hit) { + return hit.copy + } + + const copy = Array.isArray(obj) ? [] : {} + // put the copy into cache at first + // because we want to refer it in recursive deepCopy + cache.push({ + original: obj, + copy + }) + + Object.keys(obj).forEach(key => { + copy[key] = deepCopy(obj[key], cache) + }) + + return copy +} + /** * Check whether the given value is Object or not * diff --git a/test/e2e/todomvc.js b/test/e2e/todomvc.js index c7085174c..811905d67 100644 --- a/test/e2e/todomvc.js +++ b/test/e2e/todomvc.js @@ -2,6 +2,7 @@ casper.test.begin('todomvc', 57, function (test) { casper .start('examples/todomvc/index.html') .then(function () { + this.viewport(1000, 1000) // for appearing destroy button by mouse hover test.assertNotVisible('.main', '.main should be hidden') test.assertNotVisible('.footer', '.footer should be hidden') test.assertElementCount('.filters .selected', 1, 'should have one filter selected') @@ -29,7 +30,7 @@ casper.test.begin('todomvc', 57, function (test) { test.assertVisible('.main', '.main should now be visible') test.assertVisible('.footer', '.footer should now be visible') test.assertNotVisible('.clear-completed', '.clear-completed should be hidden') - test.assertField({type: 'css', path: '.new-todo'}, '', 'new todo input should be reset') + test.assertField({ type: 'css', path: '.new-todo' }, '', 'new todo input should be reset') }) // add another item --------------------------------------------------- @@ -88,6 +89,9 @@ casper.test.begin('todomvc', 57, function (test) { // remove a completed item -------------------------------------------- + .then(function () { + this.mouse.move('.todo:nth-child(1)') + }) .thenClick('.todo:nth-child(1) .destroy', function () { test.assertElementCount('.todo', 4, 'should have 4 items now') test.assertElementCount('.todo.completed', 2, 'should have 2 item completed') @@ -96,6 +100,9 @@ casper.test.begin('todomvc', 57, function (test) { // remove a incompleted item ------------------------------------------ + .then(function () { + this.mouse.move('.todo:nth-child(2)') + }) .thenClick('.todo:nth-child(2) .destroy', function () { test.assertElementCount('.todo', 3, 'should have 3 items now') test.assertElementCount('.todo.completed', 2, 'should have 2 item completed') diff --git a/test/unit/util.js b/test/unit/util.js new file mode 100644 index 000000000..1ce62fea3 --- /dev/null +++ b/test/unit/util.js @@ -0,0 +1,42 @@ +import { expect } from 'chai' +import { deepCopy } from '../../src/util' + +describe('util', () => { + it('deepCopy: nornal structure', () => { + const original = { + a: 1, + b: 'string', + c: true, + d: null, + e: undefined + } + const copy = deepCopy(original) + + expect(copy).to.deep.equal(original) + }) + + it('deepCopy: nested structure', () => { + const original = { + a: { + b: 1, + c: [2, 3, { + d: 4 + }] + } + } + const copy = deepCopy(original) + + expect(copy).to.deep.equal(original) + }) + + it('deepCopy: circular structure', () => { + const original = { + a: 1 + } + original.circular = original + + const copy = deepCopy(original) + + expect(copy).to.deep.equal(original) + }) +})