From e945934832867a4455f7b391b806f9df8efe206f Mon Sep 17 00:00:00 2001 From: wuhy Date: Sun, 20 Jan 2019 22:49:20 +0800 Subject: [PATCH] feat(okam-core): add vuex support and reafctor array observable definition --- packages/okam-core/package-lock.json | 8 +- packages/okam-core/package.json | 3 +- .../src/extend/data/{redux => }/equal.js | 2 +- .../data/observable/ComputedObserver.js | 5 + .../src/extend/data/observable/Observer.js | 67 +- .../src/extend/data/observable/ant/array.js | 22 + .../src/extend/data/observable/ant/index.js | 94 +-- .../src/extend/data/observable/array.js | 92 +-- .../src/extend/data/observable/base.js | 309 ++++----- .../src/extend/data/observable/index.js | 10 +- .../src/extend/data/observable/quick/index.js | 1 - .../src/extend/data/observable/swan/index.js | 17 +- .../src/extend/data/redux/connect.js | 2 +- .../okam-core/src/extend/data/vuex/Vue.js | 102 +++ .../okam-core/src/extend/data/vuex/index.js | 112 ++++ packages/okam-core/test/helper.js | 16 + .../extend/data/{redux => }/equal.spec.js | 2 +- .../extend/data/observable/ant/array.spec.js | 10 +- .../extend/data/observable/ant/index.spec.js | 10 +- .../extend/data/observable/array.spec.js | 5 - .../tasks/extend/data/observable/helper.js | 13 - .../test/tasks/extend/data/vuex/Vue.spec.js | 134 ++++ .../test/tasks/extend/data/vuex/index.spec.js | 628 ++++++++++++++++++ .../extend/data/vuex/store/simpleStore.js | 41 ++ .../tasks/extend/data/vuex/store/store2.js | 60 ++ .../tasks/extend/data/vuex/store/store3.js | 77 +++ .../tasks/extend/data/vuex/store/store4.js | 79 +++ .../tasks/extend/data/vuex/store/store5.js | 81 +++ .../tasks/extend/data/vuex/store/store6.js | 16 + 29 files changed, 1663 insertions(+), 355 deletions(-) rename packages/okam-core/src/extend/data/{redux => }/equal.js (96%) create mode 100644 packages/okam-core/src/extend/data/vuex/Vue.js create mode 100644 packages/okam-core/src/extend/data/vuex/index.js rename packages/okam-core/test/tasks/extend/data/{redux => }/equal.spec.js (94%) create mode 100644 packages/okam-core/test/tasks/extend/data/vuex/Vue.spec.js create mode 100644 packages/okam-core/test/tasks/extend/data/vuex/index.spec.js create mode 100644 packages/okam-core/test/tasks/extend/data/vuex/store/simpleStore.js create mode 100644 packages/okam-core/test/tasks/extend/data/vuex/store/store2.js create mode 100644 packages/okam-core/test/tasks/extend/data/vuex/store/store3.js create mode 100644 packages/okam-core/test/tasks/extend/data/vuex/store/store4.js create mode 100644 packages/okam-core/test/tasks/extend/data/vuex/store/store5.js create mode 100644 packages/okam-core/test/tasks/extend/data/vuex/store/store6.js diff --git a/packages/okam-core/package-lock.json b/packages/okam-core/package-lock.json index 6aea6319..f4f3e1bd 100644 --- a/packages/okam-core/package-lock.json +++ b/packages/okam-core/package-lock.json @@ -1,6 +1,6 @@ { "name": "okam-core", - "version": "0.4.0-beta.5", + "version": "0.4.9", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1658,6 +1658,12 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "vuex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.0.tgz", + "integrity": "sha512-mdHeHT/7u4BncpUZMlxNaIdcN/HIt1GsGG5LKByArvYG/v6DvHcOxvDCts+7SRdCoIRGllK8IMZvQtQXLppDYg==", + "dev": true + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/packages/okam-core/package.json b/packages/okam-core/package.json index df8cf052..48a84a26 100644 --- a/packages/okam-core/package.json +++ b/packages/okam-core/package.json @@ -28,6 +28,7 @@ "nyc": "^13.0.1", "promise-polyfill": "^8.0.0", "redux": "^4.0.1", - "regenerator-runtime": "^0.12.1" + "regenerator-runtime": "^0.12.1", + "vuex": "^3.1.0" } } diff --git a/packages/okam-core/src/extend/data/redux/equal.js b/packages/okam-core/src/extend/data/equal.js similarity index 96% rename from packages/okam-core/src/extend/data/redux/equal.js rename to packages/okam-core/src/extend/data/equal.js index 3a0d8ddd..e6e2d6fe 100644 --- a/packages/okam-core/src/extend/data/redux/equal.js +++ b/packages/okam-core/src/extend/data/equal.js @@ -14,7 +14,7 @@ 'use strict'; -import {isPlainObject} from '../../../util/index'; +import {isPlainObject} from '../../util/index'; function isShadowEqual(obj1, obj2) { let keys = Object.keys(obj1); diff --git a/packages/okam-core/src/extend/data/observable/ComputedObserver.js b/packages/okam-core/src/extend/data/observable/ComputedObserver.js index 0454df3a..08d6dacf 100644 --- a/packages/okam-core/src/extend/data/observable/ComputedObserver.js +++ b/packages/okam-core/src/extend/data/observable/ComputedObserver.js @@ -41,6 +41,8 @@ export default class ComputedObserver { this.computed = this.initComputedProps(computedInfo); this.watchComputed = {}; + ctx.data || (ctx.data = {}); + let watcher = this.handleDepChange.bind(this); ctx.$dataListener.on('change', watcher); this.computedCounter = 0; @@ -331,9 +333,12 @@ export default class ComputedObserver { value = ctx.data[k]; } + // maybe the computed prop is dependence on other computed props which + // has not collected deps yet, we need to call getter to collect deps if (!this.deps[k]) { let getter = this.computed[k].getter; value = getter.call(ctx, ctx); + // watch computed props is not view data, so don't put it to data watchInfo || (ctx.data[k] = value); } diff --git a/packages/okam-core/src/extend/data/observable/Observer.js b/packages/okam-core/src/extend/data/observable/Observer.js index a48bec4e..607273c2 100644 --- a/packages/okam-core/src/extend/data/observable/Observer.js +++ b/packages/okam-core/src/extend/data/observable/Observer.js @@ -5,9 +5,62 @@ 'use strict'; -import makeArrayObservable from './array'; import {addDep, getDataSelector, addSelectorPath} from './helper'; +/** + * Update array item value + * + * @param {Observer} observer the observer + * @param {number} idx the index to update + * @param {*} value the value to set + */ +function updateArrayItem(observer, idx, value) { + observer.set(idx, value); + this[idx] = value; +} + +/** + * Get the array item value + * + * @param {Observer} observer the observer + * @param {number} idx the index to get + * @return {*} + */ +function getArrayItem(observer, idx) { + return observer.get(idx); +} + +/** + * Make array observable + * + * @param {Array} arr the array to observe + * @param {Observer} observer the observer + * @param {Object} proxyArrApis the array api to proxy + * @return {Array} + */ +function makeArrayObservable(arr, observer, proxyArrApis) { + // Here, not using __proto__ implementation, there are two import reasons: + // First, considering __proto__ will be deprecated and is not recommended to use + // Second, some plugins like weixin contact plugin will change array definition, + // the array instance __proto__ property does not contains any like `push` + // `pop` API, and these API only existed in the instance context. + // So, like the code as the following will not work correctly, + // a = []; // a.__proto__.push is not defined, a.push is defined + // a.__proto__.push = function () {}; + // a.push(2); // always call the native push, not the override push method + // Therefor, using __proto__ to proxy the array method will not work + + Object.keys(proxyArrApis).forEach(method => { + let rawMethod = arr[method]; + arr[method] = proxyArrApis[method].bind(arr, observer, rawMethod); + }); + + arr.setItem = updateArrayItem.bind(arr, observer); + arr.getItem = getArrayItem.bind(arr, observer); + + return arr; +} + /** * Proxy the data object to observe * @@ -47,12 +100,12 @@ export function proxyObject(observer, data, root) { * * @param {Observer} observer the observer to observe array * @param {Array} arr the array data to proxy - * @param {boolean} isPage whether is page component + * @param {Object} proxyArrApis the array api to proxy * @return {Array} */ -export function proxyArray(observer, arr, isPage) { +export function proxyArray(observer, arr, proxyArrApis) { let newArr = []; - makeArrayObservable(newArr, observer, isPage); + makeArrayObservable(newArr, observer, proxyArrApis); // XXX: copy array // we cannot proxy array element visited by index, so we should not proxy array element by default @@ -157,7 +210,11 @@ export default class Observer { if (Array.isArray(value)) { paths || (paths = this.getPaths(k)); let observer = new Observer(ctx, value, paths, this.isProps); - return (observeData[k] = proxyArray(observer, value, ctx.$isPage)); + let proxyApis = ctx.__proxyArrayApis; + if (typeof proxyApis === 'function') { + proxyApis = ctx.__proxyArrayApis = proxyApis(); + } + return (observeData[k] = proxyArray(observer, value, proxyApis)); } else if (value && typeof value === 'object') { paths || (paths = this.getPaths(k)); diff --git a/packages/okam-core/src/extend/data/observable/ant/array.js b/packages/okam-core/src/extend/data/observable/ant/array.js index 5cd813db..73c168e1 100644 --- a/packages/okam-core/src/extend/data/observable/ant/array.js +++ b/packages/okam-core/src/extend/data/observable/ant/array.js @@ -75,6 +75,28 @@ Object.keys(observableArray).forEach(k => { }; }); +Object.assign(observableArray, { + sort(observer, rawSort, ...args) { + let rawData = observer.rawData; + rawSort.apply(rawData, args); + + let result = rawSort.apply(this, args); + observer.set(null, rawData); + + return result; + }, + + reverse(observer, rawReverse) { + let rawData = observer.rawData; + rawData.reverse(); + + let result = rawReverse.call(this); + observer.set(null, rawData); + + return result; + } +}); + export { observableArray as array, componentApi as component diff --git a/packages/okam-core/src/extend/data/observable/ant/index.js b/packages/okam-core/src/extend/data/observable/ant/index.js index 09aa2a79..e0442e99 100644 --- a/packages/okam-core/src/extend/data/observable/ant/index.js +++ b/packages/okam-core/src/extend/data/observable/ant/index.js @@ -6,54 +6,56 @@ 'use strict'; import observable, {setObservableContext} from '../base'; -import {observableArray, overrideArrayMethods} from '../array'; -import {component as antApi, array as antArray} from './array'; +import {component as antApi, array as proxyArrayApis} from './array'; setObservableContext('props', true); -let componentExtension = observable.component; -let rawCreated = componentExtension.created; -componentExtension.created = function () { - if (this.$rawComputed) { - // fix ant reference bug: `this.data.xx` operation is not allowed - // when page onload, otherwise it'll affect the init data state - // of the page when load next time. - // So, here create a shadow copy of data. - this.data = Object.assign({}, this.data); - } - rawCreated.call(this); -}; - -Object.assign(componentExtension.methods, antApi); - -let arrApis = Object.assign({}, observableArray, antArray); -overrideArrayMethods(arrApis, true); -overrideArrayMethods(arrApis, false); - -/** - * View update hook - * - * @private - * @param {Object} prevProps the previous property data before update - */ -observable.component.didUpdate = function (prevProps) { - let propObserver = this.__propsObserver; - if (!propObserver) { - return; - } - - let currProps = this.props; - // update the cache props data, as for the prop data will be override - // when prop change, it leads to the cache props data will not refer to - // the new props data - propObserver.rawData = currProps; - Object.keys(prevProps).forEach(k => { - let newVal = currProps[k]; - let oldVal = prevProps[k]; - if (newVal !== oldVal) { - propObserver.firePropValueChange(k, newVal, oldVal); +const rawCreated = observable.created; +const observableAntComponent = Object.assign({}, observable, { + created() { + if (this.$rawComputed) { + // fix ant reference bug: `this.data.xx` operation is not allowed + // when page onload, otherwise it'll affect the init data state + // of the page when load next time. + // So, here create a shadow copy of data. + this.data = Object.assign({}, this.data); + } + rawCreated.call(this); + }, + + /** + * View update hook + * + * @private + * @param {Object} prevProps the previous property data before update + */ + didUpdate(prevProps) { + let propObserver = this.__propsObserver; + if (!propObserver) { + return; } - }); -}; -export default observable; + let currProps = this.props; + // update the cache props data, as for the prop data will be override + // when prop change, it leads to the cached props data will not refer to + // the new props data + propObserver.rawData = currProps; + Object.keys(prevProps).forEach(k => { + let newVal = currProps[k]; + let oldVal = prevProps[k]; + if (newVal !== oldVal) { + propObserver.firePropValueChange(k, newVal, oldVal); + } + }); + }, + + proxyArrayApis +}); + +observableAntComponent.methods = Object.assign( + {}, observableAntComponent.methods, antApi, +); + +export default { + component: observableAntComponent +}; diff --git a/packages/okam-core/src/extend/data/observable/array.js b/packages/okam-core/src/extend/data/observable/array.js index 4e8a9bf7..bd4f110f 100644 --- a/packages/okam-core/src/extend/data/observable/array.js +++ b/packages/okam-core/src/extend/data/observable/array.js @@ -1,18 +1,16 @@ /** - * @file Array data update proxy + * @file Array proxy apis * @author sparklewhy@gmail.com */ 'use strict'; -// const hasProto = '__proto__' in {}; - /** * The default override Array APIs to proxy array data update * * @type {Object} */ -export const observableArray = { +export default { push(observer, rawPush, ...items) { let rawData = observer.rawData; let idx = rawData.length; @@ -94,89 +92,3 @@ export const observableArray = { return result; } }; - -/** - * The Page Array APIs to override - * - * @inner - * @type {Object} - */ -let overridePageArrApis = observableArray; - -/** - * The component Array APIs to override - * - * @inner - * @type {Object} - */ -let overrideComponentArrApis = observableArray; - -/** - * Extend the array operation methods - * - * @param {Object} arrApis the array methods to override - * @param {boolean} isPage whether is page Array APIs to override - */ -export function overrideArrayMethods(arrApis, isPage) { - if (isPage) { - overridePageArrApis = arrApis; - } - else { - overrideComponentArrApis = arrApis; - } -} - -/** - * Update array item value - * - * @param {Observer} observer the observer - * @param {number} idx the index to update - * @param {*} value the value to set - */ -function updateArrayItem(observer, idx, value) { - observer.set(idx, value); - this[idx] = value; -} - -/** - * Get the array item value - * - * @param {Observer} observer the observer - * @param {number} idx the index to get - * @return {*} - */ -function getArrayItem(observer, idx) { - return observer.get(idx); -} - -/** - * Make array observable - * - * @param {Array} arr the array to observe - * @param {Observer} observer the observer - * @param {boolean} isPage whether is page Array APIs to override - * @return {Array} - */ -export default function makeArrayObservable(arr, observer, isPage) { - // Here, not using __proto__ implementation, there are two import reasons: - // First, considering __proto__ will be deprecated and is not recommended to use - // Second, some plugins like weixin contact plugin will change array definition, - // the array instance __proto__ property does not contains any like `push` - // `pop` API, and these API only existed in the instance context. - // So, like the code as the following will not work correctly, - // a = []; // a.__proto__.push is not defined, a.push is defined - // a.__proto__.push = function () {}; - // a.push(2); // always call the native push, not the override push method - // Therefor, using __proto__ to proxy the array method will not work - - let overrideArrApis = isPage ? overridePageArrApis : overrideComponentArrApis; - Object.keys(overrideArrApis).forEach(method => { - let rawMethod = arr[method]; - arr[method] = overrideArrApis[method].bind(arr, observer, rawMethod); - }); - - arr.setItem = updateArrayItem.bind(arr, observer); - arr.getItem = getArrayItem.bind(arr, observer); - - return arr; -} diff --git a/packages/okam-core/src/extend/data/observable/base.js b/packages/okam-core/src/extend/data/observable/base.js index 62be9daf..5e7c9027 100644 --- a/packages/okam-core/src/extend/data/observable/base.js +++ b/packages/okam-core/src/extend/data/observable/base.js @@ -126,188 +126,191 @@ export function setObservableContext(key, ignoreUpdateHook) { } export default { - component: { + + /** + * Initialize the props to add observer to the prop to listen the prop change. + * + * @param {boolean} isPage whether is page component + */ + $init(isPage) { + // normalize extend computed property + normalizeExtendProp(this, 'computed', '$rawComputed', isPage); + normalizeExtendProp(this, 'proxyArrayApis', '__proxyArrayApis', isPage); + + // cache the raw props information because the mini program will merge data + // and props later on. + let props = this.props; + if (!props) { + return; + } + + let rawProps = Object.assign({}, props); + this._rawProps = rawProps; + normalizeExtendProp(this, '_rawProps', '$rawProps', isPage); + + this.__initProps && this.__initProps(); + }, + + /** + * The created hook + * + * @private + */ + created() { + this.__waitingSetDataQueue = []; + this.__dataUpTaskNum = 0; + + if (typeof this.__proxyArrayMethods === 'function') { + this.__proxyArrayMethods = this.__proxyArrayMethods(); + } + + // init nextTick callback + this.__nextTickCallback = this.__notifySetDataDone.bind(this); + this.__executeDataUpdate = this.__doDataUpdate.bind(this); + + this.$dataListener = new EventListener(); + this.__propsObserver = makePropsObservable(this); + this.__dataObserver = makeDataObservable(this); + + let computedObserver = this.__computedObserver + = makeComputedObservable(this); + // init computed data + this.__lazyInitCompute || computedObserver.initComputedPropValues(); + + this.__afterObserverInit && this.__afterObserverInit(); + }, + + /** + * The detached hook + * + * @private + */ + detached() { + this.__setDataQueue = null; + this.__upDoneCallbackQueue = null; + + this.$dataListener.dispose(); + this.$dataListener = null; + this.__computedObserver && this.__computedObserver.dispose(); + this.__propsObserver = this.__dataObserver = this.__computedObserver = null; + }, + + methods: { /** - * Initialize the props to add observer to the prop to listen the prop change. + * Defer the callback to be executed after the next view updated cycle. + * The callback context will be bind to the component instance + * when executed. * - * @param {boolean} isPage whether is page component + * @param {Function} callback the callback to execute */ - $init(isPage) { - // normalize extend computed property - normalizeExtendProp(this, 'computed', '$rawComputed', isPage); - - // cache the raw props information because the mini program will merge data - // and props later on. - let props = this.props; - if (!props) { - return; + $nextTick(callback) { + let queues = this.__upDoneCallbackQueue; + if (!queues) { + queues = this.__upDoneCallbackQueue = []; } - let rawProps = Object.assign({}, props); - this._rawProps = rawProps; - normalizeExtendProp(this, '_rawProps', '$rawProps', isPage); + queues.push(callback); + }, - this.__initProps && this.__initProps(); + /** + * Update computed property value + * + * @param {string} p the computed property name to update + * @param {Function=} shouldUpdate whether should update the computed property + */ + __updateComputed(p, shouldUpdate) { + let observer = this.__computedObserver; + observer && observer.updateComputed(p, shouldUpdate); }, /** - * The created hook + * Notify setData done * * @private */ - created() { - this.__waitingSetDataQueue = []; - this.__dataUpTaskNum = 0; - - // init nextTick callback - this.__nextTickCallback = this.__notifySetDataDone.bind(this); - this.__executeDataUpdate = this.__doDataUpdate.bind(this); + __notifySetDataDone() { + if (this.$isDestroyed || this.__dataUpTaskNum === 0) { + return; + } - this.$dataListener = new EventListener(); - this.__propsObserver = makePropsObservable(this); - this.__dataObserver = makeDataObservable(this); + this.__dataUpTaskNum--; + if (this.__dataUpTaskNum > 0) { + return; + } - let computedObserver = this.__computedObserver - = makeComputedObservable(this); - // init computed data - computedObserver.initComputedPropValues(); + this.__dataUpTaskNum = 0; + let queues = this.__upDoneCallbackQueue; + /* istanbul ignore next */ + if (queues) { + let num = queues.length; + while (num > 0) { + let callback = queues.shift(); + callback.call(this); + num--; + } + } - this.__afterObserverInit && this.__afterObserverInit(); + // call lifecycle updated hook + shouldSkipUpdateHook || (this.updated && this.updated()); }, /** - * The detached hook + * Execute setData operation to update view * * @private */ - detached() { + __doDataUpdate() { + if (this.$isDestroyed) { + return; + } + + let queues = this.__setDataQueue; this.__setDataQueue = null; - this.__upDoneCallbackQueue = null; + if (!queues || !queues.length) { + return; + } - this.$dataListener.dispose(); - this.$dataListener = null; - this.__computedObserver && this.__computedObserver.dispose(); - this.__propsObserver = this.__dataObserver = this.__computedObserver = null; + // call lifecycle beforeUpdate hook + this.beforeUpdate && this.beforeUpdate(); + this.setData(getSetDataPaths(queues), this.__nextTickCallback); }, - methods: { - - /** - * Defer the callback to be executed after the next view updated cycle. - * The callback context will be bind to the component instance - * when executed. - * - * @param {Function} callback the callback to execute - */ - $nextTick(callback) { - let queues = this.__upDoneCallbackQueue; - if (!queues) { - queues = this.__upDoneCallbackQueue = []; - } - - queues.push(callback); - }, - - /** - * Update computed property value - * - * @param {string} p the computed property name to update - * @param {Function=} shouldUpdate whether should update the computed property - */ - __updateComputed(p, shouldUpdate) { - let observer = this.__computedObserver; - observer && observer.updateComputed(p, shouldUpdate); - }, - - /** - * Notify setData done - * - * @private - */ - __notifySetDataDone() { - if (this.$isDestroyed || this.__dataUpTaskNum === 0) { - return; - } - - this.__dataUpTaskNum--; - if (this.__dataUpTaskNum > 0) { - return; - } - - this.__dataUpTaskNum = 0; - let queues = this.__upDoneCallbackQueue; - /* istanbul ignore next */ - if (queues) { - let num = queues.length; - while (num > 0) { - let callback = queues.shift(); - callback.call(this); - num--; - } - } - - // call lifecycle updated hook - shouldSkipUpdateHook || (this.updated && this.updated()); - }, - - /** - * Execute setData operation to update view - * - * @private - */ - __doDataUpdate() { - if (this.$isDestroyed) { - return; - } - - let queues = this.__setDataQueue; - this.__setDataQueue = null; - if (!queues || !queues.length) { - return; - } + /** + * Set the view data. It'll not update the view immediately, it's deferred + * to execute when enter the next event loop. + * + * @private + * @param {Object} obj the data to set + */ + __setViewData(obj) { + let queues = this.__setDataQueue; + let isUpdating = !!queues; + queues || (queues = this.__setDataQueue = []); + queues.push(obj); + + if (!isUpdating) { + this.__dataUpTaskNum++; + nextTick(this.__executeDataUpdate); + } + }, - // call lifecycle beforeUpdate hook - this.beforeUpdate && this.beforeUpdate(); - this.setData(getSetDataPaths(queues), this.__nextTickCallback); - }, - - /** - * Set the view data. It'll not update the view immediately, it's deferred - * to execute when enter the next event loop. - * - * @private - * @param {Object} obj the data to set - */ - __setViewData(obj) { - let queues = this.__setDataQueue; - let isUpdating = !!queues; - queues || (queues = this.__setDataQueue = []); - queues.push(obj); - - if (!isUpdating) { - this.__dataUpTaskNum++; - nextTick(this.__executeDataUpdate); - } - }, - - /** - * Set the view data. It'll not update the view immediately, it's deferred - * to execute when enter the next event loop. - * - * @private - * @param {string|Object} obj the data to set or the path to set - * @param {*=} value the new value to set, optional - */ - $setData(obj, value) { - console.warn('cannot call this API directly, it is private and will be deprecated in future'); - - if (typeof obj === 'string') { - obj = {[obj]: value}; - } + /** + * Set the view data. It'll not update the view immediately, it's deferred + * to execute when enter the next event loop. + * + * @private + * @param {string|Object} obj the data to set or the path to set + * @param {*=} value the new value to set, optional + */ + $setData(obj, value) { + console.warn('cannot call this API directly, it is private and will be deprecated in future'); - this.__setViewData(obj); + if (typeof obj === 'string') { + obj = {[obj]: value}; } + + this.__setViewData(obj); } } }; diff --git a/packages/okam-core/src/extend/data/observable/index.js b/packages/okam-core/src/extend/data/observable/index.js index fe8f8688..0be3888d 100644 --- a/packages/okam-core/src/extend/data/observable/index.js +++ b/packages/okam-core/src/extend/data/observable/index.js @@ -7,7 +7,11 @@ import observable from './base'; import initProps from './initProps'; +import proxyArrayApis from './array'; -observable.component.__initProps = initProps; - -export default observable; +export default { + component: Object.assign({}, observable, { + __initProps: initProps, + proxyArrayApis + }) +}; diff --git a/packages/okam-core/src/extend/data/observable/quick/index.js b/packages/okam-core/src/extend/data/observable/quick/index.js index 6c0acb45..0f92c813 100644 --- a/packages/okam-core/src/extend/data/observable/quick/index.js +++ b/packages/okam-core/src/extend/data/observable/quick/index.js @@ -5,7 +5,6 @@ 'use strict'; -import {getDataByPath} from '../../../../helper/data'; import watchDataChange from './watch'; /** diff --git a/packages/okam-core/src/extend/data/observable/swan/index.js b/packages/okam-core/src/extend/data/observable/swan/index.js index 86830270..a784d81e 100644 --- a/packages/okam-core/src/extend/data/observable/swan/index.js +++ b/packages/okam-core/src/extend/data/observable/swan/index.js @@ -5,21 +5,6 @@ 'use strict'; -import observable from '../base'; -// import {observableArray, overrideArrayMethods} from '../array'; -// import {component as swanApi, array as swanArray} from './array'; -import initProps from '../initProps'; - -// observable.page = { -// methods: swanApi -// }; -// Object.assign(observable.component.methods, swanApi); - -// override the Page array API, as for the native Array data operation API -// only supported in page currently -// let arrApis = Object.assign({}, observableArray, swanArray); -// overrideArrayMethods(arrApis, true); - -observable.component.__initProps = initProps; +import observable from '../index'; export default observable; diff --git a/packages/okam-core/src/extend/data/redux/connect.js b/packages/okam-core/src/extend/data/redux/connect.js index bd5cc8e6..6b2557cd 100644 --- a/packages/okam-core/src/extend/data/redux/connect.js +++ b/packages/okam-core/src/extend/data/redux/connect.js @@ -7,7 +7,7 @@ /* eslint-disable fecs-prefer-destructure */ -import isValueEqual from './equal'; +import isValueEqual from '../equal'; function normalizeStoreComputed(stateMap) { let computed; diff --git a/packages/okam-core/src/extend/data/vuex/Vue.js b/packages/okam-core/src/extend/data/vuex/Vue.js new file mode 100644 index 00000000..b95532fa --- /dev/null +++ b/packages/okam-core/src/extend/data/vuex/Vue.js @@ -0,0 +1,102 @@ +/** + * @file Create Fake Vue + * @author wuhuiyao@baidu.com + */ + +import watch from '../watch/index'; +import observable from '../observable/index'; +import proxyArrayApis from '../observable/array'; + +const observableComp = observable.component; +const watchComp = watch.component; + +/* eslint-disable fecs-prefer-class */ +/** + * Create fake Vue context + * + * @param {Object} options the vue instance options + * @return {Object} + */ +function Vue(options) { + let instance = Object.assign({}, options, observableComp.methods, watchComp.methods); + instance.created = observableComp.created; + instance.$destroy = observableComp.detached; + + // do not call setData API as for it's only an observable data instance, no view + instance.__setViewData = () => {}; + // do not initialize computed props when created + instance.__lazyInitCompute = true; + instance.__proxyArrayApis = proxyArrayApis; + + // avoid methods defined on methods, so here pass `true` + observableComp.$init.call(instance, true); + watchComp.$init.call(instance, true); + + instance.created(); + + // define observable data and exported on `_data` attribute (Vuex need it) + let data = {}; + let rawData = options && options.data; + if (rawData) { + let def = {}; + Object.keys(rawData).forEach(k => { + let descriptor = Object.getOwnPropertyDescriptor(instance, k); + def[k] = descriptor; + }); + Object.defineProperties(data, def); + } + instance._data = data; + + return instance; +} + +Vue.version = '2.5.1'; + +Vue.options = {}; + +Vue.config = { + silent: true +}; + +Vue.mixin = function (extension) { +}; + +Vue.nextTick = function (callback) { + setTimeout(callback); +}; + +Vue.set = function (target, prop, value) { + if (Array.isArray(target)) { + target.length = Math.max(target.length, prop); + target.splice(prop, 1, value); + return value; + } + target[prop] = value; + + // dynamically add responsive object property is not supported +}; + +Vue.delete = function (target, prop) { + if (Array.isArray(target)) { + target.splice(prop, 1); + return; + } + + if (target.hasOwnProperty(prop)) { + delete target[prop]; + } + + // dynamically delete responsive object property is not supported +}; + +Vue.use = function (plugin) { + let args = [Vue]; + if (typeof plugin.install === 'function') { + plugin.install.apply(plugin, args); + } + else if (typeof plugin === 'function') { + plugin.apply(null, args); + } +}; + +export default Vue; diff --git a/packages/okam-core/src/extend/data/vuex/index.js b/packages/okam-core/src/extend/data/vuex/index.js new file mode 100644 index 00000000..98779536 --- /dev/null +++ b/packages/okam-core/src/extend/data/vuex/index.js @@ -0,0 +1,112 @@ +/** + * @file Make the store observable + * Notice: this plugin should used after the observable plugin if using + * computed property + * @author sparklewhy@gmail.com + */ + +'use strict'; + +import Vuex from 'vuex'; +import Vue from './Vue'; +import isValueEqual from '../equal'; + +function onShow() { + let computedKeys = Object.keys(this.$rawComputed || {}); + if (computedKeys.length && !this.__unsubscribeStore) { + this.__unsubscribeStore = this.$store.subscribe( + this.$fireStoreChange + ); + } +} + +function onHide() { + let unsubscribe = this.__unsubscribeStore; + if (unsubscribe) { + unsubscribe(); + this.__unsubscribeStore = null; + } +} + +function shouldUpdate(old, curr) { + return !isValueEqual(old, curr); +} + +function onStoreChange() { + let upKeys = Object.keys(this.$rawComputed || {}); + let updateComputed = this.__updateComputed; + /* istanbul ignore next */ + if (updateComputed && upKeys) { + upKeys.forEach(k => updateComputed.call(this, k, shouldUpdate)); + } +} + +// should use it at first, vuex store should be created after vuex plugin is enabled +Vue.use(Vuex); + +export default { + + component: { + + /** + * The created hook when component created + * + * @private + */ + beforeCreate() { + let store = this.$app.$store; + this.$fireStoreChange = onStoreChange.bind(this); + this.$store = store; + + let computedInfo = this.$rawComputed || {}; + if (typeof computedInfo === 'function') { + this.$rawComputed = computedInfo = computedInfo(); + } + + let computedKeys = Object.keys(this.$rawComputed || {}); + if (computedKeys.length) { + this.__unsubscribeStore = store.subscribe( + this.$fireStoreChange + ); + } + }, + + /** + * OnShow hook for page component + * + * @private + */ + onShow() { + onShow.call(this); + }, + + /** + * OnHide hook for page component + * + * @private + */ + onHide() { + onHide.call(this); + }, + + /** + * Page lifetimes hook for component + * + * @private + */ + pageLifetimes: { + show: onShow, + hide: onHide + }, + + /** + * The detached hook + * + * @private + */ + detached() { + onHide.call(this); + this.$store = null; + } + } +}; diff --git a/packages/okam-core/test/helper.js b/packages/okam-core/test/helper.js index 7d181c66..9b163846 100644 --- a/packages/okam-core/test/helper.js +++ b/packages/okam-core/test/helper.js @@ -159,3 +159,19 @@ export function fakeAppEnvAPIs(appType) { base.$api = rawApi; }; } + +export function executeSequentially(taskList, initDelay) { + let result = Promise.resolve(); + taskList.forEach(task => { + let delay = task.delay || 0; + result = result.then(() => { + return new Promise((resolve, reject) => { + setTimeout(() => { + task(); + resolve(); + }, delay); + }); + }); + }); + return result.catch(ex => console.error(ex)); +} diff --git a/packages/okam-core/test/tasks/extend/data/redux/equal.spec.js b/packages/okam-core/test/tasks/extend/data/equal.spec.js similarity index 94% rename from packages/okam-core/test/tasks/extend/data/redux/equal.spec.js rename to packages/okam-core/test/tasks/extend/data/equal.spec.js index 937f1ef6..1ae84c3c 100644 --- a/packages/okam-core/test/tasks/extend/data/redux/equal.spec.js +++ b/packages/okam-core/test/tasks/extend/data/equal.spec.js @@ -6,7 +6,7 @@ 'use strict'; import assert from 'assert'; -import isEqual from 'core/extend/data/redux/equal'; +import isEqual from 'core/extend/data/equal'; describe('redux equal check', function () { it('should check equal', () => { diff --git a/packages/okam-core/test/tasks/extend/data/observable/ant/array.spec.js b/packages/okam-core/test/tasks/extend/data/observable/ant/array.spec.js index 2740f39a..b0096ec6 100644 --- a/packages/okam-core/test/tasks/extend/data/observable/ant/array.spec.js +++ b/packages/okam-core/test/tasks/extend/data/observable/ant/array.spec.js @@ -16,21 +16,13 @@ import component from 'core/ant/base/component'; import {clearBaseCache} from 'core/helper/factory'; import observable from 'core/extend/data/observable/ant'; import {fakeAntComponent, fakeAppEnvAPIs} from 'test/helper'; -import {resetObservableArray, initAntObservableArray, fakeAntArrayAPIs} from '../helper'; +import {fakeAntArrayAPIs} from '../helper'; describe('Ant array observable', function () { let MyComponent; let restoreAntArrayApi; let restoreAppEnv; - before('init observable array', function () { - initAntObservableArray(); - }); - - after('restore observable array', function () { - resetObservableArray(); - }); - beforeEach('init global App', function () { clearBaseCache(); diff --git a/packages/okam-core/test/tasks/extend/data/observable/ant/index.spec.js b/packages/okam-core/test/tasks/extend/data/observable/ant/index.spec.js index 97c0de80..6e9fa423 100644 --- a/packages/okam-core/test/tasks/extend/data/observable/ant/index.spec.js +++ b/packages/okam-core/test/tasks/extend/data/observable/ant/index.spec.js @@ -17,21 +17,13 @@ import {clearBaseCache} from 'core/helper/factory'; import {setObservableContext} from 'core/extend/data/observable/base'; import observable from 'core/extend/data/observable/ant'; import {fakeAntComponent, fakeAppEnvAPIs} from 'test/helper'; -import {resetObservableArray, initAntObservableArray, fakeAntArrayAPIs} from '../helper'; +import {fakeAntArrayAPIs} from '../helper'; describe('ant observable', function () { let MyComponent; let restoreAppEnv; let restoreAntArrayApi; - before('init observable array', function () { - initAntObservableArray(); - }); - - after('restore observable array', function () { - resetObservableArray(); - }); - beforeEach('init global App', function () { clearBaseCache(); diff --git a/packages/okam-core/test/tasks/extend/data/observable/array.spec.js b/packages/okam-core/test/tasks/extend/data/observable/array.spec.js index 92969de6..7c68ae13 100644 --- a/packages/okam-core/test/tasks/extend/data/observable/array.spec.js +++ b/packages/okam-core/test/tasks/extend/data/observable/array.spec.js @@ -14,16 +14,11 @@ import MyApp from 'core/swan/App'; import {clearBaseCache} from 'core/helper/factory'; import observable from 'core/extend/data/observable'; import {fakeComponent, fakeAppEnvAPIs} from 'test/helper'; -import {resetObservableArray} from './helper'; describe('observable array', function () { let MyComponent; let restoreAppEnv; - before('init observable array', function () { - resetObservableArray(); - }); - beforeEach('init global App', function () { clearBaseCache(); diff --git a/packages/okam-core/test/tasks/extend/data/observable/helper.js b/packages/okam-core/test/tasks/extend/data/observable/helper.js index 3abfabc3..d70c2638 100644 --- a/packages/okam-core/test/tasks/extend/data/observable/helper.js +++ b/packages/okam-core/test/tasks/extend/data/observable/helper.js @@ -5,21 +5,8 @@ 'use strict'; -import {observableArray, overrideArrayMethods} from 'core/extend/data/observable/array'; -import {array as antArray} from 'core/extend/data/observable/ant/array'; import antComponent from 'core/ant/base/component'; -export function initAntObservableArray() { - let arrApis = Object.assign({}, observableArray, antArray); - overrideArrayMethods(arrApis, true); - overrideArrayMethods(arrApis, false); -} - -export function resetObservableArray() { - overrideArrayMethods(observableArray, true); - overrideArrayMethods(observableArray, false); -} - export function fakeAntArrayAPIs() { const componentFakeMethods = [ '$spliceData' diff --git a/packages/okam-core/test/tasks/extend/data/vuex/Vue.spec.js b/packages/okam-core/test/tasks/extend/data/vuex/Vue.spec.js new file mode 100644 index 00000000..63195a45 --- /dev/null +++ b/packages/okam-core/test/tasks/extend/data/vuex/Vue.spec.js @@ -0,0 +1,134 @@ +/** + * @file Vue fake test spec + * @author sparklewhy@gmail.com + */ + +'use strict'; + +import assert from 'assert'; +import expect, {createSpy} from 'expect'; +import Vue from 'core/extend/data/vuex/Vue'; + +describe('Vue Fake', function () { + + it('Vue.set', function () { + let target = {}; + Vue.set(target, 'a', 3); + expect(target).toEqual({a: 3}); + + Vue.set(target, 'a', 6); + expect(target).toEqual({a: 6}); + + target = []; + Vue.set(target, 1, 2); + /* eslint-disable no-sparse-arrays */ + expect(target).toEqual([, 2]); + Vue.set(target, 0, 9); + expect(target).toEqual([9, 2]); + }); + + it('Vue.delete', function () { + let target = {}; + Vue.delete(target, 'a'); + expect(target).toEqual({}); + + target = {b: {a: 3}, c: 3}; + Vue.delete(target, 'b'); + expect(target).toEqual({c: 3}); + + target = []; + Vue.delete(target, 1); + expect(target).toEqual([]); + + target = [2, 3, 5]; + Vue.delete(target, 1); + expect(target).toEqual([2, 5]); + + target = [39, 52]; + Vue.delete(target, '1'); + expect(target).toEqual([39]); + }); + + it('Vue.use', function () { + let spyInstall = createSpy(() => {}); + let plugin = { + install: spyInstall + }; + + Vue.use(plugin); + assert(spyInstall.calls.length === 1); + expect(spyInstall).toHaveBeenCalledWith(Vue); + + spyInstall = createSpy(() => {}); + plugin = spyInstall; + Vue.use(plugin); + assert(spyInstall.calls.length === 1); + expect(spyInstall).toHaveBeenCalledWith(Vue); + + plugin = {}; + Vue.use(plugin); + expect(plugin).toEqual({}); + }); + + it('nextTick', function (done) { + let spyCallback = createSpy(() => {}); + Vue.nextTick(spyCallback); + + expect(spyCallback).toNotHaveBeenCalled(); + setTimeout(() => { + assert(spyCallback.calls.length === 1); + expect(spyCallback).toHaveBeenCalled(); + done(); + }, 5); + }); + + it('Vue should have specified props', function () { + assert(Vue.version === '2.5.1'); + expect(Vue.options).toEqual({}); + expect(Vue.config).toEqual({silent: true}); + assert(typeof Vue.mixin === 'function'); + }); + + it('should return observable object', function () { + let result = new Vue({ + data: { + a: 3, + arr: [], + obj: {c: 2} + }, + computed: { + b() { + return this.a + 10; + }, + c() { + return this.obj.c; + }, + arrLen() { + return this.arr.length; + } + } + }); + + assert(result.a === 3); + assert(result.b === 13); + assert(result.c === 2); + assert(result.arrLen === 0); + + result.a = 8; + result.arr.push(2); + result.arr.pop(); + result.arr.unshift(19); + result.arr.shift(); + result.arr.push(12); + result.arr.splice(0, 1, 8); + result.arr.unshift(12); + result.arr.reverse(); + expect(result.arr).toEqual([8, 12]); + assert(result.a === 8); + assert(result.b === 18); + assert(result.arrLen === 2); + + result.obj.c = 8; + assert(result.c === 8); + }); +}); diff --git a/packages/okam-core/test/tasks/extend/data/vuex/index.spec.js b/packages/okam-core/test/tasks/extend/data/vuex/index.spec.js new file mode 100644 index 00000000..2980d89a --- /dev/null +++ b/packages/okam-core/test/tasks/extend/data/vuex/index.spec.js @@ -0,0 +1,628 @@ +/** + * @file Vuex support test spec + * @author sparklewhy@gmail.com + */ + +'use strict'; + +/* eslint-disable babel/new-cap */ +/* eslint-disable fecs-properties-quote */ + +import assert from 'assert'; +import expect, {createSpy} from 'expect'; +import MyApp from 'core/swan/App'; +import MyPage from 'core/swan/Page'; +import * as na from 'core/na/index'; +import {clearBaseCache} from 'core/helper/factory'; +import vuexPlugin from 'core/extend/data/vuex/index'; +import observable from 'core/extend/data/observable'; +import store from './store/simpleStore'; +import store2 from './store/store2'; +import store3 from './store/store3'; +import store4 from './store/store4'; +import store5 from './store/store5'; +import store6 from './store/store6'; +import Vuex, {mapState, mapGetters, mapMutations, mapActions} from 'vuex'; +import {executeSequentially, fakeComponent, fakeAppEnvAPIs} from 'test/helper'; + +describe('vuex', function () { + let restoreAppEnv; + let rawGetCurrApp; + let MyComponent; + beforeEach('init global App', function () { + clearBaseCache(); + + MyComponent = fakeComponent(); + restoreAppEnv = fakeAppEnvAPIs('swan'); + MyApp.use(observable); + MyApp.use(vuexPlugin); + + rawGetCurrApp = na.getCurrApp; + }); + + afterEach('clear global App', function () { + restoreAppEnv(); + MyComponent = undefined; + na.getCurrApp = rawGetCurrApp; + expect.restoreSpies(); + }); + + it('should support using vuex manage data for page', function (done) { + na.getCurrApp = function () { + return { + $store: store + }; + }; + + let page = MyPage({ + data: { + num: 1 + }, + + computed: { + count() { + return store.state.count; + }, + a() { + return store.state.obj.a; + }, + arrLen() { + return store.state.arr.length; + }, + ...mapState({ + // arrow functions can make the code very succinct! + count2: state => (state.count + 10), + + // passing the string value 'count' is same as `state => state.count` + countAlias: 'count', + + count3(state) { + return this.countPlusLocalState + state.count; + }, + + // to access local state with `this`, a normal function must be used + countPlusLocalState(state) { + return state.count + this.num; + } + }) + }, + + methods: { + decrement() { + store.commit('decrement'); + }, + + ...mapMutations({ + add: 'increment', + changeObj: 'changeObj', + upArr: 'upArr' + }), + ...mapActions([ + 'addTwiceAction' + ]) + }, + + created() { + let state = this.$store.state; + expect(state).toEqual({ + count: 0, obj: {a: 1}, arr: [] + }); + } + }); + + let spySetData = createSpy(() => {}); + page.setData = spySetData; + + page.onLoad(); + + assert(page.count === 0); + assert(page.count2 === 10); + assert(page.count3 === 1); + assert(page.a === 1); + assert(page.countAlias === page.count); + assert(page.countPlusLocalState === 1); + + let task1 = () => { + expect(spySetData).toHaveBeenCalled(); + assert(spySetData.calls.length === 1); + let args = spySetData.calls[0].arguments; + expect(args.slice(0, args.length - 1)).toEqual([{ + a: 1, + arrLen: 0, + count: 0, + count2: 10, + count3: 1, + countAlias: 0, + countPlusLocalState: 1 + }]); + + page.add(); + page.add(); + page.decrement(); + page.add(); + page.changeObj(); + page.upArr(); + + assert(page.count === 2); + assert(page.count2 === 12); + assert(page.count3 === 5); + assert(page.countAlias === page.count); + assert(page.countPlusLocalState === 3); + assert(page.a === 2); + assert(page.arrLen === 1); + expect(page.$store.state).toEqual({ + count: 2, obj: {a: 2}, arr: [2] + }); + }; + let task2 = () => { + assert(spySetData.calls.length === 2); + let args = spySetData.calls[1].arguments; + expect(args.slice(0, args.length - 1)).toEqual([{ + count: 2, + count2: 12, + count3: 5, + countAlias: 2, + countPlusLocalState: 3, + a: 2, + arrLen: 1 + }]); + + page.num = 10; + assert(page.count === 2); + assert(page.count2 === 12); + assert(page.count3 === 14); + assert(page.countAlias === page.count); + assert(page.countPlusLocalState === 12); + }; + let task3 = () => { + assert(spySetData.calls.length === 3); + let args = spySetData.calls[2].arguments; + expect(args.slice(0, args.length - 1)).toEqual([{ + num: 10, + countPlusLocalState: 12, + count3: 14 + }]); + + page.addTwiceAction(); + }; + + let task4 = () => { + assert(page.count === 4); + assert(page.count2 === 14); + assert(page.count3 === 18); + assert(page.countAlias === page.count); + assert(page.countPlusLocalState === 14); + + assert(spySetData.calls.length === 5); + let args = spySetData.calls[3].arguments; + expect(args.slice(0, args.length - 1)).toEqual([{ + count: 3, + count2: 13, + countAlias: 3, + count3: 16, + countPlusLocalState: 13 + }]); + + args = spySetData.calls[4].arguments; + expect(args.slice(0, args.length - 1)).toEqual([{ + count: 4, + count2: 14, + countAlias: 4, + count3: 18, + countPlusLocalState: 14 + }]); + + done(); + }; + task4.delay = 10; + + executeSequentially([ + task1, task2, task3, task4 + ], 1); + }); + + it('should support using vuex manage data for component', function (done) { + na.getCurrApp = function () { + return { + $store: store2 + }; + }; + + let instance = MyComponent({ + computed: { + count() { + return this.$store.state.count; + }, + ...mapGetters([ + 'evenOrOdd' + ]) + }, + mounted() { + let state = this.$store.state; + expect(state).toEqual({count: 0}); + }, + methods: { + ...mapActions([ + 'increment', + 'decrement', + 'incrementIfOdd', + 'incrementAsync' + ]) + } + }); + + let spySetData = createSpy(() => {}); + instance.setData = spySetData; + + instance.created(); + instance.attached(); + instance.ready(); + + assert(instance.count === 0); + assert(instance.evenOrOdd === 'even'); + + setTimeout(() => { + expect(spySetData).toHaveBeenCalled(); + assert(spySetData.calls.length === 1); + let args = spySetData.calls[0].arguments; + expect(args.slice(0, args.length - 1)).toEqual([{ + count: 0, evenOrOdd: 'even' + }]); + + instance.incrementIfOdd(); + instance.increment(); + instance.decrement(); + instance.increment(); + assert(instance.count === 1); + assert(instance.evenOrOdd === 'odd'); + + instance.incrementIfOdd(); + assert(instance.count === 2); + assert(instance.evenOrOdd === 'even'); + + expect(instance.$store.state).toEqual({count: 2}); + + instance.incrementAsync(); + setTimeout(() => { + assert(spySetData.calls.length === 3); + + args = spySetData.calls[1].arguments; + expect(args.slice(0, args.length - 1)).toEqual([{ + count: 2, + evenOrOdd: 'even' + }]); + + args = spySetData.calls[2].arguments; + expect(args.slice(0, args.length - 1)).toEqual([{ + count: 3, + evenOrOdd: 'odd' + }]); + assert(instance.count === 3); + assert(instance.evenOrOdd === 'odd'); + + na.getCurrApp = rawGetCurrApp; + done(); + }, 10); + }); + + }); + + it('should support vuex module', function (done) { + na.getCurrApp = function () { + return { + $store: store3 + }; + }; + + let instance = MyComponent({ + computed: { + count() { + return this.$store.state.count; + }, + ...mapGetters({ + 'evenOrOdd': 'evenOrOdd', + moduleEventOrOdd: 'evenOrOdd2', + a: 'a' + }) + }, + mounted() { + let state = this.$store.state; + expect(state).toEqual({ + count: 0, + countModule: { + count: -99, + obj: { + a: 2 + } + } + }); + }, + methods: { + ...mapActions([ + 'increment' + ]), + ...mapMutations([ + 'add' + ]) + } + }); + + let spySetData = createSpy(() => {}); + instance.setData = spySetData; + + instance.created(); + instance.attached(); + instance.ready(); + + assert(instance.count === 0); + assert(instance.evenOrOdd === 'even'); + assert(instance.moduleEventOrOdd === 'odd'); + assert(instance.a === 2); + + instance.increment(); + instance.add(); + + assert(instance.count === 1); + assert(instance.evenOrOdd === 'odd'); + assert(instance.moduleEventOrOdd === 'odd'); + assert(instance.a === 3); + + setTimeout(() => { + expect(spySetData).toHaveBeenCalled(); + assert(spySetData.calls.length === 1); + let args = spySetData.calls[0].arguments; + expect(args.slice(0, args.length - 1)).toEqual([{ + count: 1, + a: 3, + evenOrOdd: 'odd', + moduleEventOrOdd: 'odd' + }]); + + done(); + }); + }); + + it('should support namespaced vuex module', function (done) { + na.getCurrApp = function () { + return { + $store: store4 + }; + }; + + let instance = MyComponent({ + computed: { + count() { + return this.$store.state.count; + }, + ...mapState('countModule', { + moduleCount: state => state.count + }), + ...mapGetters(['evenOrOdd']), + ...mapGetters('countModule', { + 'moduleEvenOrOdd': 'evenOrOdd', + a: 'a' + }) + }, + mounted() { + let state = this.$store.state; + expect(state).toEqual({ + count: 0, + countModule: { + count: -99, + obj: { + a: 2 + } + } + }); + }, + methods: { + ...mapActions([ + 'increment' + ]), + ...mapMutations('countModule', { + addObjA: 'add', + 'moduleAdd': 'increment' + }) + } + }); + + let spySetData = createSpy(() => {}); + instance.setData = spySetData; + + instance.created(); + instance.attached(); + instance.ready(); + + assert(instance.count === 0); + assert(instance.moduleCount === -99); + assert(instance.evenOrOdd === 'even'); + assert(instance.moduleEvenOrOdd === 'odd'); + assert(instance.a === 2); + + instance.increment(); + instance.addObjA(); + instance.moduleAdd(); + + assert(instance.count === 1); + assert(instance.moduleCount === -98); + assert(instance.evenOrOdd === 'odd'); + assert(instance.moduleEvenOrOdd === 'even'); + assert(instance.a === 3); + + setTimeout(() => { + expect(spySetData).toHaveBeenCalled(); + assert(spySetData.calls.length === 1); + let args = spySetData.calls[0].arguments; + expect(args.slice(0, args.length - 1)).toEqual([{ + count: 1, + moduleCount: -98, + a: 3, + evenOrOdd: 'odd', + moduleEvenOrOdd: 'even' + }]); + + done(); + }); + }); + + it('should support dynamically add vuex module', function (done) { + na.getCurrApp = function () { + return { + $store: store5 + }; + }; + + let instance = MyComponent({ + computed: { + count() { + let state = this.$store.state; + let dynamicModule = state.dynamicModule; + return state.count + (dynamicModule ? dynamicModule.num : 0); + } + }, + mounted() { + let state = this.$store.state; + expect(state).toEqual({ + count: 0, + countModule: { + count: -99, + obj: { + a: 2 + } + } + }); + }, + methods: { + ...mapActions([ + 'increment' + ]), + addModule() { + this.$store.registerModule('dynamicModule', { + namespaced: true, + state: { + num: 2 + }, + mutations: { + addNum: state => state.num++ + } + }); + }, + changeDynamicModuleState() { + this.$store.commit('dynamicModule/addNum'); + } + } + }); + + let spySetData = createSpy(() => {}); + instance.setData = spySetData; + + instance.created(); + instance.attached(); + instance.ready(); + + assert(instance.count === 0); + + instance.addModule(); + instance.changeDynamicModuleState(); + + expect(instance.$store.state).toEqual({ + count: 0, + countModule: {count: -99, obj: {a: 2}}, + dynamicModule: {num: 3} + }); + + setTimeout(() => { + expect(spySetData).toHaveBeenCalled(); + assert(spySetData.calls.length === 1); + let args = spySetData.calls[0].arguments; + expect(args.slice(0, args.length - 1)).toEqual([{ + count: 3 + }]); + + done(); + }); + }); + + it('should remove store watcher when component onHid or destroyed', function () { + na.getCurrApp = function () { + return { + $store: store6 + }; + }; + + let instance = MyComponent({ + computed: { + count() { + return this.$store.state.count; + } + }, + methods: { + ...mapActions([ + 'increment' + ]) + } + }); + + instance.created(); + instance.attached(); + instance.ready(); + + assert(instance.count === 0); + assert(typeof instance.__unsubscribeStore === 'function'); + + let spyUnsubscribe = createSpy(() => {}); + instance.__unsubscribeStore = spyUnsubscribe; + + instance.onHide(); + assert(instance.__unsubscribeStore === null); + expect(spyUnsubscribe).toHaveBeenCalled(); + + instance.onShow(); + assert(typeof instance.__unsubscribeStore === 'function'); + + spyUnsubscribe = createSpy(() => {}); + instance.__unsubscribeStore = spyUnsubscribe; + + instance.detached(); + + assert(instance.__unsubscribeStore === null); + assert(instance.$store === null); + expect(spyUnsubscribe).toHaveBeenCalled(); + }); + + it('should do nothing when none computed props', function () { + na.getCurrApp = function () { + return { + $store: new Vuex.Store({ + state: { + count: 0 + } + }) + }; + }; + + let instance = MyComponent({data: {count: 10}}); + + instance.created(); + instance.attached(); + instance.ready(); + + assert(instance.count === 10); + assert(!!instance.$store); + assert(instance.__unsubscribeStore === undefined); + + instance.onHide(); + assert(instance.__unsubscribeStore === undefined); + + instance.onShow(); + assert(instance.__unsubscribeStore === undefined); + + instance.$fireStoreChange(); + assert(instance.count === 10); + + instance.detached(); + + assert(instance.__unsubscribeStore === undefined); + assert(instance.$store === null); + }); +}); diff --git a/packages/okam-core/test/tasks/extend/data/vuex/store/simpleStore.js b/packages/okam-core/test/tasks/extend/data/vuex/store/simpleStore.js new file mode 100644 index 00000000..4c7b1c5a --- /dev/null +++ b/packages/okam-core/test/tasks/extend/data/vuex/store/simpleStore.js @@ -0,0 +1,41 @@ +/** + * @file Store + * @author xxx + */ + +import Vuex from 'vuex'; + +export default new Vuex.Store({ + state: { + count: 0, + obj: { + a: 1 + }, + arr: [] + }, + mutations: { + increment: state => state.count++, + decrement: state => state.count--, + changeObj(state) { + state.obj.a = 2; + }, + upArr(state) { + state.arr.push(2); + } + }, + actions: { + incrementAction({commit}) { + return new Promise((resolve, reject) => { + setTimeout(() => { + commit('increment'); + resolve(); + }); + }); + }, + addTwiceAction({dispatch, commit}) { + return dispatch('incrementAction').then(() => { + commit('increment'); + }); + } + } +}); diff --git a/packages/okam-core/test/tasks/extend/data/vuex/store/store2.js b/packages/okam-core/test/tasks/extend/data/vuex/store/store2.js new file mode 100644 index 00000000..ef644cbf --- /dev/null +++ b/packages/okam-core/test/tasks/extend/data/vuex/store/store2.js @@ -0,0 +1,60 @@ +/** + * @file Store + * @author xxx + */ + +import Vuex from 'vuex'; + +// root state object. +// each Vuex instance is just a single state tree. +const state = { + count: 0 +}; + +// mutations are operations that actually mutates 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(); + }); + }); + } +}; + +// 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 new Vuex.Store({ + state, + getters, + actions, + mutations +}); diff --git a/packages/okam-core/test/tasks/extend/data/vuex/store/store3.js b/packages/okam-core/test/tasks/extend/data/vuex/store/store3.js new file mode 100644 index 00000000..e344c933 --- /dev/null +++ b/packages/okam-core/test/tasks/extend/data/vuex/store/store3.js @@ -0,0 +1,77 @@ +/** + * @file Store + * @author xxx + */ + +import Vuex from 'vuex'; + +// root state object. +// each Vuex instance is just a single state tree. +const state = { + count: 0 +}; + +// mutations are operations that actually mutates 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(); + }, 10); + }); + } +}; + +// getters are functions +const getters = { + evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd' +}; + +const countModule = { + state: { + count: -99, + obj: {a: 2} + }, + getters: { + evenOrOdd2: state => state.count % 2 === 0 ? 'even' : 'odd', + a: state => state.obj.a + }, + mutations: { + add: state => state.obj.a++ + } +}; + +// A Vuex instance is created by combining the state, mutations, actions, +// and getters. +export default new Vuex.Store({ + state, + getters, + actions, + mutations, + modules: { + countModule + } +}); diff --git a/packages/okam-core/test/tasks/extend/data/vuex/store/store4.js b/packages/okam-core/test/tasks/extend/data/vuex/store/store4.js new file mode 100644 index 00000000..2c7af8e6 --- /dev/null +++ b/packages/okam-core/test/tasks/extend/data/vuex/store/store4.js @@ -0,0 +1,79 @@ +/** + * @file Store + * @author xxx + */ + +import Vuex from 'vuex'; + +// root state object. +// each Vuex instance is just a single state tree. +const state = { + count: 0 +}; + +// mutations are operations that actually mutates 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(); + }, 10); + }); + } +}; + +// getters are functions +const getters = { + evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd' +}; + +const countModule = { + namespaced: true, + state: { + count: -99, + obj: {a: 2} + }, + getters: { + evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd', + a: state => state.obj.a + }, + mutations: { + increment: state => state.count++, + add: state => state.obj.a++ + } +}; + +// A Vuex instance is created by combining the state, mutations, actions, +// and getters. +export default new Vuex.Store({ + state, + getters, + actions, + mutations, + modules: { + countModule + } +}); diff --git a/packages/okam-core/test/tasks/extend/data/vuex/store/store5.js b/packages/okam-core/test/tasks/extend/data/vuex/store/store5.js new file mode 100644 index 00000000..1d01fe5b --- /dev/null +++ b/packages/okam-core/test/tasks/extend/data/vuex/store/store5.js @@ -0,0 +1,81 @@ +/** + * @file Store + * @author xxx + */ + +import Vuex from 'vuex'; +import createLogger from 'vuex/dist/logger'; + +// root state object. +// each Vuex instance is just a single state tree. +const state = { + count: 0 +}; + +// mutations are operations that actually mutates 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(); + }, 10); + }); + } +}; + +// getters are functions +const getters = { + evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd' +}; + +const countModule = { + namespaced: true, + state: { + count: -99, + obj: {a: 2} + }, + getters: { + evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd', + a: state => state.obj.a + }, + mutations: { + increment: state => state.count++, + add: state => state.obj.a++ + } +}; + +// A Vuex instance is created by combining the state, mutations, actions, +// and getters. +export default new Vuex.Store({ + state, + getters, + actions, + mutations, + modules: { + countModule + }, + plugins: [createLogger()] +}); diff --git a/packages/okam-core/test/tasks/extend/data/vuex/store/store6.js b/packages/okam-core/test/tasks/extend/data/vuex/store/store6.js new file mode 100644 index 00000000..707d5fd5 --- /dev/null +++ b/packages/okam-core/test/tasks/extend/data/vuex/store/store6.js @@ -0,0 +1,16 @@ +/** + * @file Store + * @author xxx + */ + +import Vuex from 'vuex'; + +export default new Vuex.Store({ + state: { + count: 0 + }, + mutations: { + increment: state => state.count++, + decrement: state => state.count-- + } +});