diff --git a/examples/example.js b/examples/example.js index 9b64cb6..ce391b4 100644 --- a/examples/example.js +++ b/examples/example.js @@ -10,7 +10,7 @@ class Example extends RxClass { const example = new Example({ _rx_ : { - handler: value => {console.log('-----------default subscription handler-------', value)} + // handler: value => {console.log('-----------default subscription handler-------', value)} } }) @@ -26,7 +26,7 @@ example.rxAdd('test') console.log('changing test to 20, 30 directly') example.test = 20 example.test = 30 -example.rxSubscribe('test',(prop)=>console.log('#############late subscription to \'test\' after 30 ############',prop),'custom') +example.rxSubscribe('test','custom',(prop)=>console.log('#############late subscription to \'test\' after 30 ############',prop)) console.log('changing test to 40 via set') example.set('test',40) @@ -38,26 +38,31 @@ example.another.foo = 7 console.log('direct access of another.foo',example.another.foo) console.log('access via get of another.foo',example.get('another.foo')) -console.log('making a deeper property \'some.thing\' reactive that did not exist') -example.rxAdd('some.thing') - -// console.log('some test',example.some.test) - -example.some.thing=15 +console.log('making a deeper property \'some.thing\' reactive that did not exist by setting property') +example.set('some.thing','this will be reactive') +console.log(example.$get('some')) +console.log(example.get('some')) +example.set('some.thing','still reactive') +example.some.thing='directly assigned' console.log('--------------------------') console.log('instance after adding rx to properties\n') console.log(example) console.log('--------------------------') console.log('now removing reactivity from \'test\'') -example.rxRemove('test') -example.set('test',42) +example.rxRemove('test',{confirm:true}) console.log('\'test\' now holds unreactive value') -console.log('now removing reactivity from \'test\' again') -example.rxRemove('test') console.log('--------------------------') console.log('instance after removing rx from \'test\'\n') console.log(example) console.log('--------------------------') +console.log('now removing reactivity from \'test\' again') +example.rxRemove('test',{confirm:true}) -// console.log('some test',example.some.test) +console.log('---------trying out raw $get and $set-----------') +example.a = {} +console.log('$set does to make reactive and returns value set\n',example.$set(example.a,'test.object',{do:'dah', bing:'bong'})) +console.log('getting directy:',example.a) +console.log('using $get with passed object:',example.$get(example.a.test,'object.do')) +console.log('deleting do prop only', example.$del(example.a,'test.object.do',true)) +console.log('it\'s gone see:',example.a) diff --git a/examples/traverse.js b/examples/traverse.js index 8ad3159..4a565e5 100644 --- a/examples/traverse.js +++ b/examples/traverse.js @@ -34,14 +34,19 @@ example.rxAdd('test',{traverse:true}) console.log('adding new property which is made reactive by default \'test.today.bong.why\'') example.set('test.today.bong.why','do not know') -console.log('------\'test\', raw object ------ \n', example.get('test',true)) +console.log('------\'test\', raw object ------ \n', example.$get('test')) console.log('------ \'test\', values object ------\n>', example.get('test')) +console.log(example) +console.log('now removing reactivty from \'test\'') +example.rxRemove('test',{confirm:true}) +console.log(example.$get('test')) +console.log('-------------------------') console.log('now add a completely new object to root of \'foundation\' and make it reactive') const obj = {giskard:'daneel', hari: {name:'seldon', planet:'Helcion'}} example.set('foundation',obj) -console.log('------\'foundation\', raw object ------ \n', example.get('foundation',true)) +console.log('------\'foundation\', raw object ------ \n', example.$get('foundation')) console.log('------ \'foundation\', values object ------\n>', example.get('foundation')) console.log('--------instance after doing rx----\n') diff --git a/src/rx-class.js b/src/rx-class.js index d80529b..cf47813 100644 --- a/src/rx-class.js +++ b/src/rx-class.js @@ -1,79 +1,87 @@ import { EventEmitter } from 'events' import { BehaviorSubject, from } from 'rxjs' -import { distinctUntilChanged, skip } from 'rxjs/operators' +import { distinctUntilChanged, skip} from 'rxjs/operators' // ** nested object access ** -import $get from 'get-value' -import $set from 'set-value' -import $del from 'unset-value' +import _get from 'get-value' +import _set from '@uci-utils/set-value' +import _del from 'unset-value' // ********************* import equal from 'deep-equal' import traverse from 'traverse' import isPlainObject from 'is-plain-object' export default class RxClass extends EventEmitter { - constructor(opts){ + constructor(opts={}){ super(opts) + const rxopts = opts._rx_||{} this._rx_ = { - event: opts._rx_.event || 'changed', - handler: opts._rx_.handler, + event: rxopts.event || 'changed', + handler: rxopts.handler, props: {}, hooks: [], root: '', // root path/namespace to added to all get/set paths operators:[], - skip: opts._rx_.skip == null ? 1 : opts._rx_.skip + skip: rxopts.skip == null ? 0 : rxopts.skip } } + // pass '__parent__' when getting objects for creating reactive prop + // otherwise it gets the rx prop object and/or one of it's props + _rxGetObj(path,prop) { + path = this.formatObjPath(path) + path = path.split('.') + const name = path.pop() + path = path.join('.') + let parent = path ? this.$get(path) : this // if path is empty string return this + // console.log(name,path,parent) + if (parent === null) parent = {} + if (prop==='__parent__') return {name:name, parent:parent, parentPath:path} + const rx = this.$get(['_rx_.props',path,name]) + // console.log(['_rx_.props',path,name],rx) + return prop ? (rx || {})[prop] : rx + } // if property exists it moves value to _ version and creates getter and setter and behavior subject rxAdd(opath,opts={}) { - const path = this._rxFormatPath(opath) - if (this.isRx(path)) { - console.log(path, 'is already reactive, aborting rxAdd') - return - } - // console.log('making reactive', path,opts) + const path = this.formatObjPath(opath) if (opts.traverse) { - const obj = opts.values ? opts.values : $get(this,path) + const obj = opts.values ? opts.values : this.$get(path) const self = this // console.log('object to traverse', obj) traverse(obj).map(function () { if (this.isLeaf) { - // let lpath = [] - // if (opts.values) { - // lpath =[] - // } else { const lpath = this.path lpath.unshift(path) // console.log(`${!opts.values ? 'existing':'new'} leaf on path '${lpath}' with value: ${this.node}`) self.rxAdd(lpath,{value:this.node}) - // console.log(this.get()) } } ) - return + return true } // end traverse - const value = $get(this,path) + if (this.isRx(path)) return true + const value = this.$get(path) if (value === undefined ) { // console.log ('no current property or value for',path,'creating temporary null') - $set(this,path,null) + this.$set(path,null) } - const {parent,name} = this.rxGetObj(path,'__parent__') - $set(this,this._rxFormatPath(['_rx_.props',path]),{}) - const rx = $get(this,this._rxFormatPath(['_rx_.props',path])) + const {parent,name} = this._rxGetObj(path,'__parent__') + this.$set(['_rx_.props',path],{new:true}) + let rx = this.$get(this.formatObjPath(['_rx_.props',path])) // console.log('moving',opts.value != null ? opts.value : value,path) rx.value = opts.value != null ? opts.value : value rx.path = path - rx.obs = from(new BehaviorSubject({value:value, path:path}).pipe( + rx.obs = from(new BehaviorSubject({value:rx.value, path:rx.path}).pipe( // rx.obs = from(new ReplaySubject(1).pipe( skip(this._rx_.skip), distinctUntilChanged() // , // takeUntil($get(this.$deleted,path)) )) + // console.log(path,'---------------\n',Object.getOwnPropertyNames(Object.getPrototypeOf(rx.obs)),rx.obs.subscribe) rx.subs = {} - this.rxSubscribe(rx,this._rx_.handler,'_default') - this.rxSubscribe(rx,(opts.subscribe ||{}).handler,(opts.subscribe || {}).name) + this.rxSubscribe(rx,'_default',this._rx_.handler,) + this.rxSubscribe(rx,(opts.subscribe || {}).name,(opts.subscribe ||{}).handler) const self = this Object.defineProperty(parent, name, { configurable: true, @@ -82,6 +90,7 @@ export default class RxClass extends EventEmitter { return rx.value }, set (value) { + // console.log('in setter', path,value) rx.value = value rx.obs.next({value:value, path:path}) self.emit(opts.event || self._rx_.event,{value:value, path:path}) @@ -89,6 +98,7 @@ export default class RxClass extends EventEmitter { self._rx_.hooks.forEach(hook=>hook.call(self,{value:value, path:path})) } }) + return true } // if name is only passed remove @@ -101,89 +111,113 @@ export default class RxClass extends EventEmitter { this._rx_.operators[name]=func } - // pass '__new__' when getting props for new rx object - rxGetObj(path,prop) { - path = this._rxFormatPath(path) - path = path.split('.') - const name = path.pop() - path = path.join('.') - let parent = path ? $get(this,path) : this - // console.log(name,path,parent) - if (parent === null) parent = {} - if (prop==='__parent__') return {name:name, parent:parent, parentPath:path} - const rx = this.get(['_rx_.props',path,name],true) - // console.log(path,name,'reactive object',rx) - return prop ? (rx || {})[prop] : rx - } - // removes obser, subscriptions, and getter and setter and make back into plain value rxRemove(path,opts={}) { - if (opts.traverse) { + if (!opts.confirm) return false // must confirm to remove + if (!this.isRx(path) && isPlainObject(this.$get(path))) { const self = this - traverse($get(this,path)).map(function () { + traverse(this.$get(path)).map(function () { if (this.isLeaf) { const lpath = this.path lpath.unshift(path) - self.rxRemove(lpath) + if(self.isRx(lpath)) self.rxRemove(lpath,{confirm:true}) } - } - ) - this.delPath(['_rx_.props',path],true) - return + }) + // all done removing the leaves so remove branch + this.$del(['_rx_.props',path],true) + return true } - let { parent, name, parentPath } = this.rxGetObj(path,'__parent__') - const rxparent = this.get(['_rx_.props',parentPath],true) + let { parent, name, parentPath } = this._rxGetObj(path,'__parent__') + const rxparent = this.$get(['_rx_.props',parentPath]) const rx = rxparent[name] - if (rx && Object.getOwnPropertyDescriptor(parent, name)['get']) { - Object.values(rx.subs).forEach(sub=>sub.unsubscribe()) - delete parent[name] - parent[name] = rx.value - delete rxparent[name] - } // else console.warn(`property at '${path}' was not reative, remove aborted`) + if (!rx) return true // // not reactive nothing to remove + Object.values(rx.subs).forEach(sub=>sub.unsubscribe()) + delete parent[name] + parent[name] = rx.value + delete rxparent[name] + // console.log('removed rx from', path) + return true } // pass rx as object or path - rxSubscribe(rxObj,handler,name){ - if (typeof name !== 'string') return - rxObj = (typeof rxObj === 'string') ? this.rxGetObj(rxObj) : rxObj - if (typeof handler==='function') rxObj.subs[name] = rxObj.obs.subscribe(handler) + rxSubscribe(rxObj,name,handler){ + // if (name ==='ha') { + // console.log(this.id,'making subscription',name,rxObj.path || rxObj,!!handler) + // } + if (typeof name !== 'string') return false + rxObj = (typeof rxObj === 'string' || Array.isArray(rxObj) ) ? this._rxGetObj(rxObj) : rxObj + // if (name && name!=='_default' && (rxObj||{}).path) console.log('rx object for',rxObj.path) + if ((rxObj||{}).path) { + if (typeof handler==='function') { + // console.log('-----------subscription made-----------') + return rxObj.subs[name] = rxObj.obs.subscribe(handler) + } + } + return false + // else { + // if (name && name!=='_default') { + // console.log('*********no rx object at that path*******') + // console.log('================================') + // } + // } } - _rxFormatPath(path) { + // takes several formats for a path to an objects property and return only the . string + formatObjPath(path) { if (path==null) return path if (Array.isArray(path)) { - path = path.filter(Boolean).join('.') - } else path = path.replace(/\/:/g, '.') + path = path.filter(Boolean).join('.') // Boolean filter strips empty strings or null/undefined + } + path = path.replace(/\/:,/g, '.') // replaces /:, with . return path } + // checks for getter and the corresponding rx object isRx(path) { - const { parent, name, parentPath } = this.rxGetObj(path,'__parent__') - const rxparent = this.get(['_rx_.props',parentPath],true) || {} + if (this.$get(path)===undefined) return false + path = this.formatObjPath(path) + const { parent, name, parentPath } = this._rxGetObj(path,'__parent__') + const rxparent = this.$get(['_rx_.props',parentPath]) || {} const rx = rxparent[name] - // console.log ('rx parent path',rxparent, rx, !!rx && !!Object.getOwnPropertyDescriptor(parent, name)['get']) - return (!!rx && !!Object.getOwnPropertyDescriptor(parent, name)['get']) + // console.log (path, 'rxparent,rx', !!parent, !!name, !!rxparent, !!rx) + if (!rx) return false + // console.log(Object.getOwnPropertyDescriptor(parent, name)['get'] ) + return !!Object.getOwnPropertyDescriptor(parent, name)['get'] } - setObjPath () {return $set(...arguments)} - getObjPath () {return $get(...arguments)} - delObjPath () {return $del(...arguments)} - - delPath(path,confirm) { - path = this._rxFormatPath(path) - if (confirm) $del(this,path) - else console.warn('must confirm to delete path',path) + $set () { + let args = [...arguments] + if (args.length < 2) return false + if (args.length === 2) args.unshift(this) + else if (!args[0]) return false + args[1] = this.formatObjPath(args[1]) + // console.log('in $set',args[1],args[2]) + return _set(...args) + } + $get () { + let args = [...arguments] + if (args.length === 1) args.unshift(this) + args[1] = this.formatObjPath(args[1]) + return _get(...args) + } + $del () { + let args = [...arguments] + if (args.length < 2) return false + if (args[args.length-1]!==true) return false + if (args.length === 2) args.unshift(this) + args[1] = this.formatObjPath(args[1]) + return _del(...args) } - get(path,raw){ - path = this._rxFormatPath(path) - const value = $get(this,path) - if (!isPlainObject(value) || raw) return value - let obj = {} // return a copy with actual values - traverse($get(this,path)).map(function () { + get(path){ + path = this.formatObjPath(path) + const value = this.$get(path) + if (!isPlainObject(value)) return value + let obj = {} // return a copy with actual values instead of getters + const self = this + traverse(this.$get(path)).map(function () { if (this.isLeaf) { - // console.log(obj,this.path.join('.'),this.node) - $set(obj,this.path.join('.'),this.node) + self.$set(obj,this.path,this.node) } } ) @@ -191,16 +225,19 @@ export default class RxClass extends EventEmitter { } set(path,value,opts={}){ - if (!opts.noRx || !this.isRx(path)) { + // console.log(path,value,opts,!this.isRx(path)) + if (!opts.noRx && !this.isRx(path)) { opts = Object.assign(isPlainObject(value) ? {values:value,traverse:true} : {value:value},opts) this.rxAdd(path,opts) + } else { + const curValue = this.$get(path) + if (!equal(curValue,value) && value!==undefined) { + // console.log('in set', path,value) + this.$set(path,value) + // console.log('value that was set',this.$get(path)) + } + return this.$get(path) } - path = this._rxFormatPath(path) - const curValue = $get(this,path) - if (!equal(curValue,value) || !value) { - $set(this,path,value) - } - return $get(this,path) } } // end class