322 lines
12 KiB
JavaScript
322 lines
12 KiB
JavaScript
// import methods from './public-methods.js'
|
|
// native modules
|
|
import { EventEmitter } from 'events'
|
|
// reactive modules
|
|
import { BehaviorSubject, from } from 'rxjs'
|
|
import { distinctUntilChanged, skip } from 'rxjs/operators'
|
|
// uci modules
|
|
import { check } from '@uci-utils/type'
|
|
import bind from '@uci-utils/bind-funcs'
|
|
import { get as $get, set as $set, del as $del, walkPath, getKeys, pathToString } from '@uci-utils/obj-nested-prop'
|
|
import { logger } from '@uci-utils/logger'
|
|
// supporting
|
|
import equal from 'deep-equal'
|
|
import traverse from 'traverse'
|
|
|
|
const log = logger({ file: '/src/rx-class.js', package: '@uci-utils/rx-class', class: 'RxClass' })
|
|
// to pass rx options use 'Symbol.for('rx-class-opts')'
|
|
class RxClass extends EventEmitter {
|
|
constructor (opts = {}) {
|
|
super(opts)
|
|
const rxopts = opts[Symbol.for('rx-class-opts')] || {}
|
|
Object.defineProperty(this, '$$__rxns__$$', {
|
|
enumerable: false,
|
|
value: rxopts.namespace || 'rx'
|
|
})
|
|
this[this.$$__rxns__$$] = bind(methods, this)
|
|
this[this.$$__rxns__$$].props = {} // where reactive property info is stored including actual values
|
|
this[this.$$__rxns__$$].opts = {
|
|
emitter: rxopts.emitter != null ? rxopts.emitter : true,
|
|
event: rxopts.event === false ? false : rxopts.event || 'changed',
|
|
handler: rxopts.handler, // default subscription handler
|
|
amendValue: rxopts.amendValue,
|
|
// amend_path: rxopts.amend_path, // by default amend value with path
|
|
hook: rxopts.hook, // function called when setting
|
|
namespace: this.$$__rxns__$$, // TODO namespace to added to all public rx methods
|
|
operators: new Map(), // TODO added to all rx pipes, in order
|
|
skip: rxopts.skip == null ? 1 : rxopts.skip
|
|
}
|
|
console.log(`${rxopts.getSetPrefix || ''}set`)
|
|
$set(this, [rxopts.getSetNamespace, `${rxopts.getSetPrefix || ''}set`], set.bind(this))
|
|
$set(this, [rxopts.getSetNamespace, `${rxopts.getSetPrefix || ''}get`], get.bind(this))
|
|
console.log(this)
|
|
} // end constructor
|
|
// reactive methods are bound at instantiation
|
|
} // end class
|
|
|
|
// helpers
|
|
|
|
export default RxClass
|
|
export { RxClass, $set as set, $get as get, $del as del, getKeys, walkPath, pathToString }
|
|
|
|
function get (path) {
|
|
const value = _.get.call(this, path)
|
|
if (!check.isPlainObject(value)) return value
|
|
const obj = {} // return a copy with actual values instead of getters
|
|
const self = this
|
|
traverse(_.get.call(this, path)).map(function () {
|
|
if (this.isLeaf) {
|
|
_.set.call(self, obj, this.path, this.node)
|
|
}
|
|
}
|
|
)
|
|
return obj
|
|
}
|
|
|
|
function set (path, value, opts = {}) {
|
|
console.log('set:', path, value, opts, !this[this.$$__rxns__$$].isRx(path))
|
|
if (!opts.noRx && !this[this.$$__rxns__$$].isRx(path)) { // travsere
|
|
opts = Object.assign(
|
|
(check.isPlainObject(value) && opts.traverse !== false)
|
|
? { values: value, traverse: true }
|
|
: { value: value }
|
|
, opts)
|
|
console.log('adding rx from set', opts)
|
|
this[this.$$__rxns__$$].add(path, opts)
|
|
} else {
|
|
const curValue = _.get.call(this, path)
|
|
if (!equal(curValue, value) && value !== undefined) {
|
|
console.log('setting already reactive value', path, value)
|
|
_.set.call(this, path, value)
|
|
console.log('value that was set', _.get.call(this, path))
|
|
}
|
|
return _.get.call(this, path)
|
|
}
|
|
}
|
|
|
|
// public methods will be namespaced (default is rx.)
|
|
const methods = {
|
|
add: function add (path, opts = {}) {
|
|
if (opts.traverse) { // will add rx to all leaves
|
|
const obj = opts.values ? opts.values : _.get.call(this, path)
|
|
const self = this
|
|
// console.log('object to traverse', obj)
|
|
traverse(obj).map(function () {
|
|
// console.log('traversing', this)
|
|
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[this.$$__rxns__$$].add(lpath, { value: this.node })
|
|
}
|
|
}
|
|
)
|
|
return true
|
|
} // end traverse
|
|
const value = _.get.call(this, path) // current value
|
|
if (value === undefined) {
|
|
// console.log('no current property or value for', path, 'creating temporary null')
|
|
_.set.call(this, path, null)
|
|
}
|
|
if (this[this.$$__rxns__$$].isRx(path) && !opts.force) return true
|
|
console.log(path, 'making reactive')
|
|
const { parent, name } = _.rxGetObj.call(this, path, '__parent__')
|
|
log.debug({
|
|
class: 'RxClass',
|
|
method: 'rx.add',
|
|
path: path,
|
|
name: name,
|
|
parent: parent,
|
|
msg: 'got name and parent'
|
|
})
|
|
_.set.call(this, [this.$$__rxns__$$, 'props', path], {})
|
|
const rx = _.get.call(this, [this.$$__rxns__$$, '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[this.$$__rxns__$$].opts.amendValue || ((val) => val)
|
|
rx.obs = from(new BehaviorSubject(rx.amendValue(rx.value, rx.path)).pipe(
|
|
skip(opts.skip != null ? opts.skip : this[this.$$__rxns__$$].opts.skip),
|
|
distinctUntilChanged() // allow custom comparator
|
|
// allow custom/additional operators here
|
|
// takeUntil(#get(this.#deleted,path))
|
|
))
|
|
// console.log(path,'---------------\n',Object.getOwnPropertyNames(Object.getPrototypeOf(rx.obs)),rx.obs.subscribe)
|
|
rx.subs = {}
|
|
rx.hook = opts.hook
|
|
if (check.isFunction(this[this.$$__rxns__$$].opts.handler)) this[this.$$__rxns__$$].subscribe(rx, this[this.$$__rxns__$$].opts.handler, '_default_')
|
|
const subs = Object.entries(opts.subscribe || {})
|
|
subs.forEach(sub => {
|
|
if (check.isFunction(sub[1])) this[this.$$__rxns__$$].subscribe(rx, sub[1], sub[0])
|
|
})
|
|
const self = this
|
|
Object.defineProperty(parent, name, {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get () {
|
|
return rx.value
|
|
},
|
|
set (value) {
|
|
rx.value = value
|
|
value = rx.amendValue(value, path)
|
|
rx.obs.next(value) // react
|
|
if (self[this.$$__rxns__$$].opts.emitter) { // emit
|
|
if (opts.event) self.emit(opts.event, value, path) // custom event
|
|
if (self[this.$$__rxns__$$].opts.event) self.emit(self[this.$$__rxns__$$].opts.event, value, path) // global event
|
|
const spath = pathToString(path)
|
|
if (spath) self.emit(spath, value, path) // also emit path if is stringable.
|
|
self.emit(name, value, path)
|
|
}
|
|
// any hook function that is already bound will use that context, otherwise this
|
|
// hooks
|
|
// TODO write with for loop and async await
|
|
if (check.isFunction(self[this.$$__rxns__$$].opts.hook)) self[this.$$__rxns__$$].opts.hook.call(self, value) // global hook
|
|
if (check.isFunction(rx.hook)) rx.hook.call(self, value) // property hook
|
|
}
|
|
})
|
|
// console.log('rxadd done', path, opts, _.get.call(this,[this.$$__rxns__$$,'props', path]))
|
|
return true
|
|
},
|
|
|
|
amendValue: function amendValue (path, func) {
|
|
if (arguments.length === 0) { this[this.$$__rxns__$$].opts.amendValue = null; return }
|
|
if (typeof path === 'function') { this[this.$$__rxns__$$].opts.amendValue = path; return }
|
|
const rx = _.rxGetObj.call(this, path)
|
|
if (!check.isEmptyPlainObject(rx)) return false
|
|
func = func || (val => val)
|
|
rx.amendValue = func === 'path' ? (value, path) => { return { value: value, path: path } } : func
|
|
return !!rx.amendValue
|
|
},
|
|
|
|
// if name is only passed remove
|
|
operator: function Operator (func, name) {
|
|
this[this.$$__rxns__$$].opts.operators[name] = func
|
|
},
|
|
|
|
// removes obser, subscriptions, and getter and setter and make back into plain value
|
|
remove: function remove (path, opts = {}) {
|
|
if (!opts.confirm) return false // must confirm to remove
|
|
if (!this[this.$$__rxns__$$].isRx(path) && check.isPlainObject(_.get.call(this, path))) {
|
|
const self = this
|
|
traverse(_.get.call(this, path)).map(function () {
|
|
if (this.isLeaf) {
|
|
const lpath = this.path
|
|
lpath.unshift(path)
|
|
if (self[this.$$__rxns__$$].isRx(lpath)) self[this.$$__rxns__$$].remove(lpath, { confirm: true })
|
|
}
|
|
})
|
|
// all done removing the leaves so remove branch
|
|
_.del.call(this, [this.$$__rxns__$$, 'props', path], true)
|
|
return true
|
|
}
|
|
const { parent, name, parentPath } = _.rxGetObj.call(this, path, '__parent__')
|
|
const rxparent = _.get.call(this, [this.$$__rxns__$$, '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
|
|
},
|
|
|
|
getObs: function getObs (path) {
|
|
return (_.rxGetObj.call(this, path) || {}).obs
|
|
},
|
|
|
|
getSubs: function getSubs (path, name) {
|
|
const rx = _.rxGetObj.call(this, path)
|
|
if (rx) return (name ? rx.subs[name] : Object.keys(rx.subs))
|
|
},
|
|
// pass rx as object or path
|
|
subscribe: function subscribe (path, handler, name) {
|
|
const rx = _.rxGetObj.call(this, path)
|
|
if (rx) {
|
|
// TODO if no name generate a name and return with handle
|
|
if (check.isFunction(handler)) {
|
|
if (rx.subs[name]) rx.subs[name].unsubscribe() // replace if exits
|
|
rx.subs[name] = rx.obs.subscribe(handler)
|
|
// console.log(rx.path, 'current subscriptions---', Object.keys(rx.subs))
|
|
return { subs: rx.subs[name], name: name }
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
|
|
removeSubs: function removeSubs (path, name) {
|
|
const rx = _.rxGetObj.call(this, path)
|
|
// if (name && name!=='_default' && (rxObj||{}).path) console.log('rx object for',rxObj.path)
|
|
if (rx) {
|
|
if (name) {
|
|
if (rx.subs[name]) {
|
|
rx.subs[name].unsubscribe()
|
|
delete rx.subs[name]
|
|
return true
|
|
}
|
|
} else {
|
|
Object.values(rx.subs).forEach(sub => sub.unsubscribe())
|
|
rx.subs = {}
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
isGetSet: function isGetSet (path) {
|
|
const keys = getKeys(path)
|
|
const prop = keys.pop()
|
|
const parent = _.get.call(this, keys)
|
|
// console.log('isgetset', path, keys, prop, 'parent?', !!parent)
|
|
if (parent && prop) {
|
|
return !!(Object.getOwnPropertyDescriptor(parent, prop) || {}).get &&
|
|
!!(Object.getOwnPropertyDescriptor(parent, prop) || {}).set
|
|
}
|
|
return false
|
|
},
|
|
// is already reactive, checks for setter/getter and the corresponding rx object
|
|
isRx: function isRx (path) {
|
|
// console.log('in isRX', path)
|
|
const keys = getKeys(path)
|
|
const cnt = keys.length
|
|
for (let index = 0; index < cnt; index++) {
|
|
// console.log('testing rx', index, cnt, keys, !!_.get.call(this,keys), !!this.isGetSet(keys), !!_.get.call(this,[this.$$__rxns__$$,'props', keys]))
|
|
if (_.get.call(this, keys) === undefined) return false
|
|
if (this[this.$$__rxns__$$].isGetSet(keys) && _.get.call(this, [this.$$__rxns__$$, 'props', keys])) return true
|
|
keys.pop()
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
// private methods
|
|
|
|
const _ = {
|
|
rxGetObj: function rxGetObj (path, prop) {
|
|
log.debug({ class: 'RxClass', method: '_.rxGetObj', path: path, prop: prop, msg: 'getting rx object' })
|
|
let name, parent, parentPath, rx
|
|
// if (!path) parent = this
|
|
if (check.isString(path) || check.isArray(path)) { // it's a normal path
|
|
const keys = getKeys(path)
|
|
name = (keys || []).pop()
|
|
parent = _.get.call(this, keys)
|
|
parentPath = keys
|
|
log.debug({ method: '_.rxGetObj', path: path, prop: prop, key: name, parent: parent, msg: 'getting rx object' })
|
|
if (parent === null) parent = {}
|
|
if (prop === '__parent__') return { name: name, parent: parent, parentPath: parentPath }
|
|
rx = _.get.call(this, [this.$$__rxns__$$, 'props', path])
|
|
} else rx = check.isPlainObject(path) ? path : rx // if path was plain object assume it's already rx object
|
|
return prop ? (rx || {})[prop] : rx
|
|
},
|
|
set: function set () {
|
|
const args = [...arguments]
|
|
if (args.length < 2) return false
|
|
if (args.length === 2) args.unshift(this)
|
|
else if (!args[0]) return false
|
|
// console.log('in #set',args[1],args[2])
|
|
return $set(...args)
|
|
},
|
|
get: function get () {
|
|
const args = [...arguments]
|
|
if (args.length === 1) args.unshift(this)
|
|
return $get(...args)
|
|
},
|
|
del: function del () {
|
|
const args = [...arguments]
|
|
if (args.length < 2) return false
|
|
if (args[args.length - 1] !== true) return false
|
|
if (args.length === 2) args.unshift(this)
|
|
return $del(...args)
|
|
}
|
|
|
|
}
|