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 '@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={}){ super(opts) const rxopts = opts._rx_||{} this._rx_ = { event: rxopts.event || 'changed', handler: rxopts.handler, props: {}, amendValue: rxopts.amendValue, emit_path : rxopts.emit_path, hooks: [], // will add to all setters root: '', // root path/namespace to added to all get/set paths operators:[], // will add to all pipes 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.formatObjPath(opath) if (opts.traverse) { 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) { 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}) } } ) return true } // end traverse 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') this.$set(path,null) } 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.amendValue = opts.amendValue || this._rx_.amendValue || ((val) => val) if (opts.emit_path || this._rx_.emit_path) rx.amendValue = (value,path) => {return {value:value, path: path}} rx.obs = from(new BehaviorSubject(rx.amendValue(rx.value,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 = {} rx.hooks = [] 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, enumerable: true, get () { return rx.value }, set (value) { // console.log('in setter', path,value) rx.value = value value = rx.amendValue(value,path) rx.obs.next(value) self.emit(opts.event || self._rx_.event,value) self.emit(path,value) // any hook function that already bound will use that context self._rx_.hooks.forEach(hook=>hook.call(self,value)) rx.hooks.forEach(hook=>hook.call(self,value)) } }) return true } // if name is only passed remove rxHook(func,name) { this._rx_.hooks[name]=func } rxAmendValue(path,func) { let rx = {} if (arguments.length === 0 ) {this._rx_.amendValue= null;return} if (typeof path === 'function') {this._rx_.amendValue=path;return} rx = (typeof path === 'string') ? this._rxGetObj(path) : path if (!rx) return false func = func ? func : (val => val ) console.log('amend', func) rx.amendValue = func === 'path' ? (value,path) => {return {value:value, path: path}} : func return !!rx.amendValue } // if name is only passed remove rxOperator(func,name) { this._rx_.operators[name]=func } // removes obser, subscriptions, and getter and setter and make back into plain value rxRemove(path,opts={}) { if (!opts.confirm) return false // must confirm to remove if (!this.isRx(path) && isPlainObject(this.$get(path))) { const self = this traverse(this.$get(path)).map(function () { if (this.isLeaf) { const lpath = this.path lpath.unshift(path) 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__') const rxparent = this.$get(['_rx_.props',parentPath]) const rx = rxparent[name] 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,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('================================') // } // } } // 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('.') // 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) { 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 (path, 'rxparent,rx', !!parent, !!name, !!rxparent, !!rx) if (!rx) return false // console.log(Object.getOwnPropertyDescriptor(parent, name)['get'] ) return !!Object.getOwnPropertyDescriptor(parent, name)['get'] } $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){ 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) { self.$set(obj,this.path,this.node) } } ) return obj } set(path,value,opts={}){ // 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) } } } // end class