import { EventEmitter } from 'events' import { BehaviorSubject, from } from 'rxjs' 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 equal from 'deep-equal' import traverse from 'traverse' import isPlainObject from 'is-plain-object' export default class RxClass extends EventEmitter { constructor(opts){ // console.log('constructor', opts) super(opts) this._rx_ = { event: opts._rx_.event || 'changed', handler: opts._rx_.handler, props: {}, hooks: [], operators:[], skip: opts._rx_.skip == null ? 1 : opts._rx_.skip } } // 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 (opts.traverse) { const self = this traverse(_get(this,path)).map(function () { if (this.isLeaf) { const lpath = this.path lpath.unshift(path) // console.log('leaf',lpath,this.node) self.rxAdd(lpath) // console.log(this.get()) } } ) return } // end traverse const value = _get(this,path) if (value === undefined) { console.log ('no current property for', path) _set(this,path,null) } const {parent,name} = this.rxGetObj(path,'__parent__') const rx = this.setPath(['_rx_.props',path],{}) rx.value = value rx.obs = from(new BehaviorSubject({value:value, path:path}).pipe( // rx.obs = from(new ReplaySubject(1).pipe( skip(this._rx_.skip), distinctUntilChanged() // , // takeUntil(_get(this._deleted,path)) )) rx.subs = {} this.rxSubscribe(rx,this._rx_.handler,'_default') this.rxSubscribe(rx,(opts.subscribe ||{}).handler,(opts.subscribe || {}).name) const self = this Object.defineProperty(parent, name, { configurable: true, enumerable: true, get () {return rx.value}, set (value) { rx.value = value rx.obs.next({value:value, path:path}) self.emit(opts.event || self._rx_.event,{value:value, path:path}) self.emit(path,value) self._rx_.hooks.forEach(hook=>hook.call(self,{value:value, path:path})) } }) } // if name is only passed remove rxHook(func,name) { this._rx_.hooks[name]=func } // if name is only passed remove rxOperator(func,name) { 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.getPath(['_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) { const self = this traverse(_get(this,path)).map(function () { if (this.isLeaf) { const lpath = this.path lpath.unshift(path) self.rxRemove(lpath) } } ) this.delPath(['_rx_.props',path],true) return } let { parent, name, parentPath } = this.rxGetObj(path,'__parent__') const rxparent = this.getPath(['_rx_.props',parentPath],true) 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`) } // pass rx as object or path rxSubscribe(rxObj,handler,name){ // console.log('subscription',rxObj) if (typeof name !== 'string') return rxObj = (typeof rxObj === 'string') ? this.rxGetObj(rxObj) : rxObj if (typeof handler==='function') rxObj.subs[name] = rxObj.obs.subscribe(handler) } _rxFormatPath(path) { if (path==null) return path if (Array.isArray(path)) { path = path.filter(Boolean).join('.') } else path = path.replace(/\/:/g, '.') return path } 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) } getPath(path,raw){ path = this._rxFormatPath(path) const value = _get(this,path) if (!isPlainObject(value) || raw) return value let obj = {} traverse(_get(this,path)).map(function () { if (this.isLeaf) { // let leaf = this.path // this path is immutable // console.log(this.path,this.node) // leaf = leaf.pop() _set(obj,this.path.join('.'),this.node) } } ) return obj } setPath(path,value){ path = this._rxFormatPath(path) const curValue = _get(this,path) if (!equal(curValue,value) || !value) { _set(this,path,value) } return _get(this,path) } } // end class