207 lines
6.6 KiB
JavaScript
207 lines
6.6 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){
|
|
super(opts)
|
|
this._rx_ = {
|
|
event: opts._rx_.event || 'changed',
|
|
handler: opts._rx_.handler,
|
|
props: {},
|
|
hooks: [],
|
|
root: '', // root path/namespace to added to all get/set paths
|
|
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 (this.isRx(path)) {
|
|
console.log(path, 'is already reactive, aborting rxAdd')
|
|
return
|
|
}
|
|
// console.log('making reactive', path,opts)
|
|
if (opts.traverse) {
|
|
const obj = opts.values ? opts.values : $get(this,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
|
|
} // end traverse
|
|
const value = $get(this,path)
|
|
if (value === undefined ) {
|
|
// console.log ('no current property or value for',path,'creating temporary null')
|
|
$set(this,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]))
|
|
|
|
// 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 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.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) {
|
|
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.get(['_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){
|
|
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
|
|
}
|
|
|
|
isRx(path) {
|
|
const { parent, name, parentPath } = this.rxGetObj(path,'__parent__')
|
|
const rxparent = this.get(['_rx_.props',parentPath],true) || {}
|
|
const rx = rxparent[name]
|
|
// console.log ('rx parent path',rxparent, rx, !!rx && !!Object.getOwnPropertyDescriptor(parent, name)['get'])
|
|
return (!!rx && !!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)
|
|
}
|
|
|
|
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 () {
|
|
if (this.isLeaf) {
|
|
// console.log(obj,this.path.join('.'),this.node)
|
|
$set(obj,this.path.join('.'),this.node)
|
|
}
|
|
}
|
|
)
|
|
return obj
|
|
}
|
|
|
|
set(path,value,opts={}){
|
|
if (!opts.noRx || !this.isRx(path)) {
|
|
opts = Object.assign(isPlainObject(value) ? {values:value,traverse:true} : {value:value},opts)
|
|
this.rxAdd(path,opts)
|
|
}
|
|
path = this._rxFormatPath(path)
|
|
const curValue = $get(this,path)
|
|
if (!equal(curValue,value) || !value) {
|
|
$set(this,path,value)
|
|
}
|
|
return $get(this,path)
|
|
}
|
|
|
|
} // end class
|