fixed bug in isRx and this.set (needed to check for undefined)

added $get,$set,$del  any object for direct access, defaults to 'this'
finished rxRemove
master
David Kebler 2020-06-15 10:08:52 -07:00
parent 3a72d1618e
commit e0c674f3e5
3 changed files with 155 additions and 108 deletions

View File

@ -10,7 +10,7 @@ class Example extends RxClass {
const example = new Example({ const example = new Example({
_rx_ : { _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') console.log('changing test to 20, 30 directly')
example.test = 20 example.test = 20
example.test = 30 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') console.log('changing test to 40 via set')
example.set('test',40) example.set('test',40)
@ -38,26 +38,31 @@ example.another.foo = 7
console.log('direct access of another.foo',example.another.foo) console.log('direct access of another.foo',example.another.foo)
console.log('access via get of another.foo',example.get('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') console.log('making a deeper property \'some.thing\' reactive that did not exist by setting property')
example.rxAdd('some.thing') example.set('some.thing','this will be reactive')
console.log(example.$get('some'))
// console.log('some test',example.some.test) console.log(example.get('some'))
example.set('some.thing','still reactive')
example.some.thing=15 example.some.thing='directly assigned'
console.log('--------------------------') console.log('--------------------------')
console.log('instance after adding rx to properties\n') console.log('instance after adding rx to properties\n')
console.log(example) console.log(example)
console.log('--------------------------') console.log('--------------------------')
console.log('now removing reactivity from \'test\'') console.log('now removing reactivity from \'test\'')
example.rxRemove('test') example.rxRemove('test',{confirm:true})
example.set('test',42)
console.log('\'test\' now holds unreactive value') console.log('\'test\' now holds unreactive value')
console.log('now removing reactivity from \'test\' again')
example.rxRemove('test')
console.log('--------------------------') console.log('--------------------------')
console.log('instance after removing rx from \'test\'\n') console.log('instance after removing rx from \'test\'\n')
console.log(example) console.log(example)
console.log('--------------------------') 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)

View File

@ -34,14 +34,19 @@ example.rxAdd('test',{traverse:true})
console.log('adding new property which is made reactive by default \'test.today.bong.why\'') console.log('adding new property which is made reactive by default \'test.today.bong.why\'')
example.set('test.today.bong.why','do not know') 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('------ \'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') 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'}} const obj = {giskard:'daneel', hari: {name:'seldon', planet:'Helcion'}}
example.set('foundation',obj) 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('------ \'foundation\', values object ------\n>', example.get('foundation'))
console.log('--------instance after doing rx----\n') console.log('--------instance after doing rx----\n')

View File

@ -2,78 +2,86 @@ import { EventEmitter } from 'events'
import { BehaviorSubject, from } from 'rxjs' import { BehaviorSubject, from } from 'rxjs'
import { distinctUntilChanged, skip} from 'rxjs/operators' import { distinctUntilChanged, skip} from 'rxjs/operators'
// ** nested object access ** // ** nested object access **
import $get from 'get-value' import _get from 'get-value'
import $set from 'set-value' import _set from '@uci-utils/set-value'
import $del from 'unset-value' import _del from 'unset-value'
// ********************* // *********************
import equal from 'deep-equal' import equal from 'deep-equal'
import traverse from 'traverse' import traverse from 'traverse'
import isPlainObject from 'is-plain-object' import isPlainObject from 'is-plain-object'
export default class RxClass extends EventEmitter { export default class RxClass extends EventEmitter {
constructor(opts){ constructor(opts={}){
super(opts) super(opts)
const rxopts = opts._rx_||{}
this._rx_ = { this._rx_ = {
event: opts._rx_.event || 'changed', event: rxopts.event || 'changed',
handler: opts._rx_.handler, handler: rxopts.handler,
props: {}, props: {},
hooks: [], hooks: [],
root: '', // root path/namespace to added to all get/set paths root: '', // root path/namespace to added to all get/set paths
operators:[], 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 // if property exists it moves value to _ version and creates getter and setter and behavior subject
rxAdd(opath,opts={}) { rxAdd(opath,opts={}) {
const path = this._rxFormatPath(opath) const path = this.formatObjPath(opath)
if (this.isRx(path)) {
console.log(path, 'is already reactive, aborting rxAdd')
return
}
// console.log('making reactive', path,opts)
if (opts.traverse) { if (opts.traverse) {
const obj = opts.values ? opts.values : $get(this,path) const obj = opts.values ? opts.values : this.$get(path)
const self = this const self = this
// console.log('object to traverse', obj) // console.log('object to traverse', obj)
traverse(obj).map(function () { traverse(obj).map(function () {
if (this.isLeaf) { if (this.isLeaf) {
// let lpath = []
// if (opts.values) {
// lpath =[]
// } else {
const lpath = this.path const lpath = this.path
lpath.unshift(path) lpath.unshift(path)
// console.log(`${!opts.values ? 'existing':'new'} leaf on path '${lpath}' with value: ${this.node}`) // console.log(`${!opts.values ? 'existing':'new'} leaf on path '${lpath}' with value: ${this.node}`)
self.rxAdd(lpath,{value:this.node}) self.rxAdd(lpath,{value:this.node})
// console.log(this.get())
} }
} }
) )
return return true
} // end traverse } // end traverse
const value = $get(this,path) if (this.isRx(path)) return true
const value = this.$get(path)
if (value === undefined ) { if (value === undefined ) {
// console.log ('no current property or value for',path,'creating temporary null') // 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__') const {parent,name} = this._rxGetObj(path,'__parent__')
$set(this,this._rxFormatPath(['_rx_.props',path]),{}) this.$set(['_rx_.props',path],{new:true})
const rx = $get(this,this._rxFormatPath(['_rx_.props',path])) let rx = this.$get(this.formatObjPath(['_rx_.props',path]))
// console.log('moving',opts.value != null ? opts.value : value,path) // console.log('moving',opts.value != null ? opts.value : value,path)
rx.value = opts.value != null ? opts.value : value rx.value = opts.value != null ? opts.value : value
rx.path = path 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( // rx.obs = from(new ReplaySubject(1).pipe(
skip(this._rx_.skip), skip(this._rx_.skip),
distinctUntilChanged() distinctUntilChanged()
// , // ,
// takeUntil($get(this.$deleted,path)) // takeUntil($get(this.$deleted,path))
)) ))
// console.log(path,'---------------\n',Object.getOwnPropertyNames(Object.getPrototypeOf(rx.obs)),rx.obs.subscribe)
rx.subs = {} rx.subs = {}
this.rxSubscribe(rx,this._rx_.handler,'_default') this.rxSubscribe(rx,'_default',this._rx_.handler,)
this.rxSubscribe(rx,(opts.subscribe ||{}).handler,(opts.subscribe || {}).name) this.rxSubscribe(rx,(opts.subscribe || {}).name,(opts.subscribe ||{}).handler)
const self = this const self = this
Object.defineProperty(parent, name, { Object.defineProperty(parent, name, {
configurable: true, configurable: true,
@ -82,6 +90,7 @@ export default class RxClass extends EventEmitter {
return rx.value return rx.value
}, },
set (value) { set (value) {
// console.log('in setter', path,value)
rx.value = value rx.value = value
rx.obs.next({value:value, path:path}) rx.obs.next({value:value, path:path})
self.emit(opts.event || self._rx_.event,{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})) self._rx_.hooks.forEach(hook=>hook.call(self,{value:value, path:path}))
} }
}) })
return true
} }
// if name is only passed remove // if name is only passed remove
@ -101,89 +111,113 @@ export default class RxClass extends EventEmitter {
this._rx_.operators[name]=func 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 // removes obser, subscriptions, and getter and setter and make back into plain value
rxRemove(path,opts={}) { 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 const self = this
traverse($get(this,path)).map(function () { traverse(this.$get(path)).map(function () {
if (this.isLeaf) { if (this.isLeaf) {
const lpath = this.path const lpath = this.path
lpath.unshift(path) lpath.unshift(path)
self.rxRemove(lpath) if(self.isRx(lpath)) self.rxRemove(lpath,{confirm:true})
} }
})
// all done removing the leaves so remove branch
this.$del(['_rx_.props',path],true)
return true
} }
) let { parent, name, parentPath } = this._rxGetObj(path,'__parent__')
this.delPath(['_rx_.props',path],true) const rxparent = this.$get(['_rx_.props',parentPath])
return
}
let { parent, name, parentPath } = this.rxGetObj(path,'__parent__')
const rxparent = this.get(['_rx_.props',parentPath],true)
const rx = rxparent[name] const rx = rxparent[name]
if (rx && Object.getOwnPropertyDescriptor(parent, name)['get']) { if (!rx) return true // // not reactive nothing to remove
Object.values(rx.subs).forEach(sub=>sub.unsubscribe()) Object.values(rx.subs).forEach(sub=>sub.unsubscribe())
delete parent[name] delete parent[name]
parent[name] = rx.value parent[name] = rx.value
delete rxparent[name] delete rxparent[name]
} // else console.warn(`property at '${path}' was not reative, remove aborted`) // console.log('removed rx from', path)
return true
} }
// pass rx as object or path // pass rx as object or path
rxSubscribe(rxObj,handler,name){ rxSubscribe(rxObj,name,handler){
if (typeof name !== 'string') return // if (name ==='ha') {
rxObj = (typeof rxObj === 'string') ? this.rxGetObj(rxObj) : rxObj // console.log(this.id,'making subscription',name,rxObj.path || rxObj,!!handler)
if (typeof handler==='function') rxObj.subs[name] = rxObj.obs.subscribe(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 (path==null) return path
if (Array.isArray(path)) { if (Array.isArray(path)) {
path = path.filter(Boolean).join('.') path = path.filter(Boolean).join('.') // Boolean filter strips empty strings or null/undefined
} else path = path.replace(/\/:/g, '.') }
path = path.replace(/\/:,/g, '.') // replaces /:, with .
return path return path
} }
// checks for getter and the corresponding rx object
isRx(path) { isRx(path) {
const { parent, name, parentPath } = this.rxGetObj(path,'__parent__') if (this.$get(path)===undefined) return false
const rxparent = this.get(['_rx_.props',parentPath],true) || {} path = this.formatObjPath(path)
const { parent, name, parentPath } = this._rxGetObj(path,'__parent__')
const rxparent = this.$get(['_rx_.props',parentPath]) || {}
const rx = rxparent[name] const rx = rxparent[name]
// console.log ('rx parent path',rxparent, rx, !!rx && !!Object.getOwnPropertyDescriptor(parent, name)['get']) // console.log (path, 'rxparent,rx', !!parent, !!name, !!rxparent, !!rx)
return (!!rx && !!Object.getOwnPropertyDescriptor(parent, name)['get']) if (!rx) return false
// console.log(Object.getOwnPropertyDescriptor(parent, name)['get'] )
return !!Object.getOwnPropertyDescriptor(parent, name)['get']
} }
setObjPath () {return $set(...arguments)} $set () {
getObjPath () {return $get(...arguments)} let args = [...arguments]
delObjPath () {return $del(...arguments)} if (args.length < 2) return false
if (args.length === 2) args.unshift(this)
delPath(path,confirm) { else if (!args[0]) return false
path = this._rxFormatPath(path) args[1] = this.formatObjPath(args[1])
if (confirm) $del(this,path) // console.log('in $set',args[1],args[2])
else console.warn('must confirm to delete path',path) 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){ get(path){
path = this._rxFormatPath(path) path = this.formatObjPath(path)
const value = $get(this,path) const value = this.$get(path)
if (!isPlainObject(value) || raw) return value if (!isPlainObject(value)) return value
let obj = {} // return a copy with actual values let obj = {} // return a copy with actual values instead of getters
traverse($get(this,path)).map(function () { const self = this
traverse(this.$get(path)).map(function () {
if (this.isLeaf) { if (this.isLeaf) {
// console.log(obj,this.path.join('.'),this.node) self.$set(obj,this.path,this.node)
$set(obj,this.path.join('.'),this.node)
} }
} }
) )
@ -191,16 +225,19 @@ export default class RxClass extends EventEmitter {
} }
set(path,value,opts={}){ 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) opts = Object.assign(isPlainObject(value) ? {values:value,traverse:true} : {value:value},opts)
this.rxAdd(path,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))
} }
path = this._rxFormatPath(path) return this.$get(path)
const curValue = $get(this,path)
if (!equal(curValue,value) || !value) {
$set(this,path,value)
} }
return $get(this,path)
} }
} // end class } // end class