278 lines
9.8 KiB
JavaScript
278 lines
9.8 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 '@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_ = {
|
|
emitter: rxopts.emitter !=null ? rxopts.emitter : true,
|
|
event: rxopts.event === false ? false : rxopts.event || 'changed',
|
|
handler: rxopts.handler,
|
|
props: {},
|
|
amendValue: rxopts.amendValue,
|
|
amend_path : rxopts.amend_path, // by default amend value with 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.amend_path || this._rx_.amend_path) rx.amendValue = (value,path) => {return {value:value, path: path}}
|
|
// console.log(path,': initial value===>',rx.amendValue(rx.value,rx.path))
|
|
rx.obs = from(new BehaviorSubject(rx.amendValue(rx.value,rx.path)).pipe(
|
|
skip(opts.skip != null ? opts.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)
|
|
if (self._rx_.emitter) {
|
|
if (opts.event) self.emit(opts.event,value)
|
|
if (self._rx_.event) self.emit(self._rx_.event,value,path)
|
|
self.emit(path,value)
|
|
self.emit(name,value,path)
|
|
}
|
|
// 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
|
|
const rx = (typeof rxObj === 'string' || Array.isArray(rxObj) ) ? this._rxGetObj(rxObj) : rxObj
|
|
// if (name && name!=='_default' && (rxObj||{}).path) console.log('rx object for',rxObj.path)
|
|
if ((rx||{}).path) {
|
|
if (typeof handler==='function') {
|
|
rx.subs[name] = rx.obs.subscribe(handler)
|
|
// console.log(rx.path, 'current subscriptions---',Object.keys(rx.subs))
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
rxUnsubscribe(rxObj,name){
|
|
if (typeof name !== 'string') return false
|
|
const rx = (typeof rxObj === 'string' || Array.isArray(rxObj) ) ? this._rxGetObj(rxObj) : rxObj
|
|
// if (name && name!=='_default' && (rxObj||{}).path) console.log('rx object for',rxObj.path)
|
|
if (rx.subs[name]){
|
|
rx.subs[name].unsubscribe()
|
|
delete rx.subs[name]
|
|
// console.log(rx.path, ':current subscriptions>',Object.keys(rx.subs))
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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)
|
|
// console.log('set opts', 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
|