uci-utils-rx-class/src/rx-class.js

182 lines
5.4 KiB
JavaScript

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