diff --git a/src/decorator/utils/getStateLifeDecorator.js b/src/decorator/utils/getStateLifeDecorator.js index df84426..30ad323 100644 --- a/src/decorator/utils/getStateLifeDecorator.js +++ b/src/decorator/utils/getStateLifeDecorator.js @@ -11,18 +11,19 @@ import { observable, isObservableArray } from 'mobx' +import logger from '../../utils/logger' export const assignState = action( function (self, property, val) { const setVal = (value = val) => { if (self[property] !== value) { - // console.log(name + ' set', property, value) + logger.debug(name + ' set', property, value) self[property] = value } } if (typeof val !== 'undefined') { - // console.log('before load url: `' + property + '`:', this[property]); + logger.debug('before load `' + property + '`:', self[property]) if (val == null) { setVal() } @@ -31,7 +32,9 @@ export const assignState = action( // remove overflow items if arrays if ( Array.isArray(val) && - (isObservableArray(self[property]) || Array.isArray(self[property])) + ( + isObservableArray(self[property]) || Array.isArray(self[property]) + ) && val.length < self[property].length ) { self[property].splice(val.length, self[property].length - val.length) @@ -58,13 +61,34 @@ export const assignState = action( setVal() } - // console.log('after loaded url: `' + property + '`:', this[property]); + logger.debug('after load: `' + property + '`:', self[property]) } } ) +// const g = {} +function extendsHideProps(target, propKey, value) { + const old = target[propKey] && target[propKey] + if (typeof old === 'object' && old !== null) { + // only saved on top parent + if (!Array.isArray(old)) { + Object.assign(old, value) + } + else { + old.push(value) + } + return + } + Object.defineProperty(target, propKey, { + value, + configurable: true, + enumerable: false + }) +} + export default (config = {}, name = 'state-life') => { config = config || {} + // const collection = g[name] = g[name] || {} return (urlKey, options = {}, target, property, descriptor) => { if (typeof urlKey !== 'string') { @@ -73,112 +97,143 @@ export default (config = {}, name = 'state-life') => { } options = options || {} const { initKey = 'init', exitKey = 'exit', updateKey } = options - const assignStateValue = function () { - return assignState.call(null, this, property, config.get(urlKey)) + const assignStateValue = function (self, property, urlKey) { + return assignState(self, property, config.get(urlKey)) } if ('value' in descriptor && typeof descriptor.value === 'function') { throw new Error('`' + name + '` can NOT use in member method') } - // maybe observable - // if ('get' in descriptor) { - // throw new Error('`' + name + '` can NOT use in getter') - // } if ('initializer' in descriptor) { - console.warn('`' + property + '`' + 'is unobservable,', name, 'would may it to be observable.') + logger.warn('`' + property + '`' + 'is unobservable,', name, 'would make it to be observable.') descriptor = observable(target, property, descriptor) } - // https://github.com/mobxjs/mobx/issues/1382 - let dispose - let syncUrlTimer - let syncUrlFn - - // eslint-disable-next-line no-inner-declarations - function release() { - dispose && dispose() - dispose = null - if (syncUrlTimer) { - clearTimeout(syncUrlTimer) - syncUrlFn && syncUrlFn() - syncUrlTimer = void 0 - syncUrlFn = void 0 + const hidePropKey = `__[[${name}_origin_hooks]]__` + const hideArrPropKey = `__[[${name}_array]]__` + // Firstly! + // Supports inheritance + if (!target[hidePropKey] || !target[hidePropKey][property]) { + const hooks = { + init: target[initKey], + update: target[updateKey], + exit: target[exitKey] } + extendsHideProps(target, hidePropKey, { + [property]: hooks + }) } - let originExit = target[exitKey] - target[exitKey] = function (...args) { - config.exit && config.exit(this, property, urlKey) - // console.log('dispose ' + name + ' `' + property + '`') - release() - return originExit && originExit.call(this, ...args) + if (!target[hideArrPropKey]) { + extendsHideProps(target, hideArrPropKey, []) } - - // eslint-disable-next-line no-use-before-define - target[initKey] = init(target[initKey], 'init') - - if (updateKey) { - target[updateKey] = ( - function (origin) { - return action(function (...args) { - assignStateValue.call(this) - return origin && origin.call(this, ...args) - }) - } - )(target[updateKey]) + let i = target[hideArrPropKey].findIndex(([p]) => p === property) + if (i >= 0) { + target[hideArrPropKey].splice(i, 1) } + const func = ( + function () { + let dispose + let syncUrlTimer + let syncUrlFn + + // eslint-disable-next-line no-inner-declarations + function release() { + dispose && dispose() + dispose = null + if (syncUrlTimer) { + clearTimeout(syncUrlTimer) + syncUrlFn && syncUrlFn() + syncUrlTimer = void 0 + syncUrlFn = void 0 + } + } - // eslint-disable-next-line no-inner-declarations,no-unused-vars - function init(origin, actionType) { - return action(function (...args) { - config.init && config.init(this, property, urlKey) - - // console.log(actionType + ' ' + name + ' `' + property + '`') - release() - assignStateValue.call(this) - - let isFirst = true - dispose = autorun( - () => { - // 一段时间内的修改以最后一次为准 - if (syncUrlTimer) { - clearTimeout(syncUrlTimer) - syncUrlTimer = void 0 - } - - let obj = { [urlKey]: this[property] } - // invoke the deep `getter` of this[property] - // noop op - try { - JSON.stringify(obj) - } catch (err) { - console.error('[Stringify]', obj, 'Error happened:', err) - } + return { + init: function () { + config.init && config.init(this, property, urlKey) + logger.debug('init ' + name + ' `' + property + '`') + release() + assignStateValue(this, property, urlKey) + + let isFirst = true + dispose = autorun(() => { + // 一段时间内的修改以最后一次为准 + if (syncUrlTimer) { + clearTimeout(syncUrlTimer) + syncUrlTimer = void 0 + } - syncUrlFn = (isFirst = false) => { - let save = isFirst ? ( - config.saveFirstTime || config.save - ) : config.save - save.call(config, urlKey, this[property], config.fetch()) - syncUrlTimer = void 0 - syncUrlFn = void 0 - } + let obj = { [urlKey]: this[property] } + // invoke the deep `getter` of this[property] + // noop op + try { + JSON.stringify(obj) + } catch (err) { + console.error('[Stringify]', obj, 'Error happened:', err) + } - if (isFirst) { - if (options.initialWrite) { - syncUrlFn(true) + syncUrlFn = (isFirst = false) => { + let save = isFirst ? ( + config.saveFirstTime || config.save + ) : config.save + // console.log('save', urlKey, property, this[property]) + save.call(config, urlKey, this[property], config.fetch()) + syncUrlTimer = void 0 + syncUrlFn = void 0 } - isFirst = false - return - } - syncUrlTimer = setTimeout(syncUrlFn, 250) + if (isFirst) { + if (options.initialWrite) { + syncUrlFn(true) + } + isFirst = false + return + } + syncUrlTimer = setTimeout(syncUrlFn, 250) + }) + }, + update: function () { + assignStateValue(this, property, urlKey) + }, + exit: function () { + config.exit && config.exit(this, property, urlKey) + logger.debug('dispose ' + name + ' `' + property + '`') + release() } - ) + } + } + )() + extendsHideProps(target, hideArrPropKey, [property, func]) + + const hooks = target[hidePropKey] + const arrays = target[hideArrPropKey] + const callHook = (self, hookName, args) => { + return typeof hooks[hookName] === 'function' + && hooks[hookName].apply(self, args) + } - return origin && origin.call(this, ...args) + target[initKey] = function (...args) { + arrays.forEach(([, { init }]) => { + init.call(this) }) + return callHook(this, initKey, args) } + if (updateKey) { + target[updateKey] = function (...args) { + arrays.forEach(([, { update }]) => { + update.call(this) + }) + return callHook(this, updateKey, args) + } + } + target[exitKey] = function (...args) { + arrays.forEach(([, { init }]) => { + init.call(this) + }) + return callHook(this, exitKey, args) + } + return descriptor && { ...descriptor, configurable: true } } } diff --git a/src/utils/logger.js b/src/utils/logger.js index e085d63..388f8a5 100644 --- a/src/utils/logger.js +++ b/src/utils/logger.js @@ -8,18 +8,14 @@ export default { debug: function (...args) { - if (process.env.NODE_ENV !== 'production') { - console.log(...args) + if (global && global.VM_DEBUG || process.env.NODE_ENV !== 'production') { + console.log('[react-mobx-vm] Debug:',...args) } }, warn: function (...args) { - if (process.env.NODE_ENV !== 'production') { - console.warn(...args) - } + console.warn('[react-mobx-vm] Warning:', ...args) }, error: function (...args) { - if (process.env.NODE_ENV !== 'production') { - console.error(...args) - } + console.error('[react-mobx-vm] Error:',...args) } } diff --git a/test/decorator-urlSync.test.js b/test/decorator-urlSync.test.js index b56c9d3..8ddd478 100644 --- a/test/decorator-urlSync.test.js +++ b/test/decorator-urlSync.test.js @@ -13,6 +13,7 @@ import * as React from 'react' import ReactDOM from 'react-dom' import { hashHistory } from 'react-router' import { stringify as oStringify, parse as oParse } from 'qs' + function stringify(obj) { return '?' + oStringify(obj) } @@ -37,6 +38,7 @@ describe('decorator-urlSync', function () { return null } } + @bindView(View) class App extends Root { @urlSync @@ -55,7 +57,7 @@ describe('decorator-urlSync', function () { } @urlSync - @observable arr = [{ a: 'a' }, 'b']; + @observable arr = [{ a: 'a' }, 'b'] @urlSync @observable @@ -69,6 +71,11 @@ describe('decorator-urlSync', function () { beforeEach(() => { dom = document.createElement('div') registerUrlSync(hashHistory) + hashHistory.push({ + path: '/', + search: '', + query: null + }) }) test('decorator-urlSync simple', async (done) => { class Simple extends App { @@ -83,6 +90,7 @@ describe('decorator-urlSync', function () { ) } } + vm = Simple.create() let root = vm.root let arr = vm.arr @@ -130,7 +138,6 @@ describe('decorator-urlSync', function () { }) }) await mockDelay() - await mockDelay() expect( JSON.stringify(vm.arr) ).toEqual( @@ -148,20 +155,67 @@ describe('decorator-urlSync', function () { }) // https://github.com/mobxjs/mobx/issues/1382 - test('extends and observable', () => { - class P { + test('extends and observable', async (done) => { + class P extends App { @urlSync @observable a = 'x'; + @urlSync + @observable b = 'y' + + @urlSync('i') + @observable int = 23 + + @urlSync('pArr') + @observable arr = ['1', '2'] @observable v = 'pv' } + class S extends P { @urlSync('xx') @observable a = 's'; + @urlSync('yy') + @observable b = 't' + @urlSync('ii') + @observable int = 222 + @urlSync('s') + @observable str = 't' @observable v = 'sv' } expect(new S().v).toBe('sv') + + // expect( + // Object.keys(s['__[[urlsync_origin_hooks]]__']) + // ).toEqual( + // ['str', 'num', 'int', 'obj', 'arr', 'root', 'a', 'b'] + // ) + vm = S.create({ a: 'abc', b: 'bbb' }) + ReactDOM.render( + <RouterV3 history={hashHistory} routes={{ path: '/', component: vm }}/>, + dom + ) + await mockDelay() + expect( + parse(hashHistory.getCurrentLocation().search) + ).toEqual({}) + + vm.a = 'update' + vm.b = 'updateB' + vm.str = 's' + vm.num = 234 + vm.int = 22222 + await mockDelay() + expect( + parse(hashHistory.getCurrentLocation().search) + ).toEqual({ + ii: '22222', + num: '234', + s: 's', + xx: 'update', + yy: 'updateB' + }) + done() }) })