support for name spacing of reactive methods and objects/fields

support for namesapce and preface of get and set
master
Kebler Network System Administrator 2021-05-13 15:34:55 -07:00
parent 4b76b80f07
commit ce0e77399a
5 changed files with 321 additions and 353 deletions

View File

@ -1,37 +0,0 @@
module.exports = {
"ecmaFeatures": {
"modules": true,
"spread" : true,
"restParams" : true
},
// "plugins": [
// "unicorn"
// ],
"env": {
"es6": true,
"node": true,
"mocha": true
},
"parserOptions": {
"ecmaVersion": 2017,
"sourceType": "module"
},
"extends": "eslint:recommended",
"rules": {
"indent": [
"error",
2
],
// "unicorn/no-array-instanceof": "error",
"no-console": 0,
"semi": ["error", "never"],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
]
}
}

View File

@ -1,59 +1,79 @@
import RxClass from '../src/rx-class.js' import RxClass from '../src/rx-class.js'
class Example extends RxClass { const log = o => console.dir(o, { depth: 5 })
constructor(opts={}){
const rxopts = Symbol.for('rx-class-opts')
class Example extends RxClass {
constructor (opts = {}) {
super(opts) super(opts)
this.test = { this.test = {
tomrrow: {bar:'fight', foo:'fighters'}, tomrrow: { bar: 'fight', foo: 'fighters' },
today: {bing:'bing', bong:{who:'first',what:'second'}} today: { bing: 'bing', bong: { who: 'first', what: 'second' } }
} }
} }
} }
const opts = {
const example = new Example({ [rxopts]: {
_rx_ : { handler: value => { console.log('-----------default subscription handler-------', value) },
handler: value => {console.log('-----------default subscription handler-------', value)},
skip: 1 skip: 1
} }
}) }
example.foundation = null console.log('options\n', opts)
console.log('-----EXAMPLE: TRAVERSE OF EXISTING OBJECT OR WHEN SETTING A PLAIN OBJECT-----------') const example = new Example(opts)
console.log('instance before doing rx on properties\n')
console.log(example)
console.log('--------------------------')
// example.on('changed', (prop) => console.log('emitted----a property changed-----',prop)) // console.log('-----EXAMPLE: TRAVERSE OF EXISTING OBJECT OR WHEN SETTING A PLAIN OBJECT-----------')
// example.on('test', (val) => console.log('emitted value of test',val)) // console.log('instance before doing rx on properties\n')
// // log(example)
// console.log('------------------------------------------')
console.log('traversing exitings object \'test\' and making all leaves reactive') // example.on('changed', (prop) => console.log('emitted----a property changed-----', prop))
console.log(example.test) // example.on('test', (val) => console.log('emitted value of test', val))
example.rxAdd('test',{traverse:true})
console.log('adding new property which is made reactive by default \'test.today.bong.why\'') // console.log('----- traversing existing object \'test\' and making all leaves reactive ------\n')
example.set('test.today.bong.why','do not know') // example.rxAdd('test', { traverse: true })
// // log(example)
// console.log('------------------------------------------')
console.log('------\'test\', raw object ------ \n', example.$get('test')) // console.log('now get value for bong directly and via get', example.test.today.bong, example.get('test.today.bong'))
console.log('------ \'test\', values object ------\n>', example.get('test')) // console.log('Note: using example.get will traverse bong and get actual values')
console.log(example)
console.log('now removing reactivty from \'test\'')
example.rxRemove('test',{confirm:true})
console.log(example.$get('test'))
console.log('-------------------------')
console.log('now add a completely new object to root of \'foundation\' and make it reactive') // console.log('adding new property which is made reactive by default \'test.today.bong.why\'')
const obj = {giskard:'daneel', hari: {name:'seldon', planet:'Helcion'}} // example.set('test.today.bong.why', 'do not know')
example.set('foundation',obj)
console.log('------\'foundation\', raw object ------ \n', example.$get('foundation')) // console.log('------\'.bong\', raw object ------ \n', example.test.today.bong)
console.log('------ \'foundation\', values object ------\n>', example.get('foundation')) // console.log('------ \'.bong\', values object ------\n>', example.get('test.today.bong'))
// console.log('------, note: raw leaf access returns value not [Getter/Setter] ==>', example.test.today.bong.why)
// console.log('\nnow removing reactivty from \'test\'')
// example.rxRemove('test', { confirm: true })
// console.log('------\'.test\', raw object ------ \n', example.test)
// console.log('--------------------------')
console.log('--------instance after doing rx----\n') console.log('\n\nnow add a completely new object to root of \'foundation\' and make it reactive without traverse')
console.log(example) example.foundation = { planets: ['Trantor', 'Helcion'], persons: { giskard: { alias: 'daneel' }, hari: { name: 'seldon', planet: 'Helcion' } } }
console.log('--------------------------') console.dir(example.foundation)
example.rxAdd('foundation.persons.hari.name')
// console.log('foundation', example.isRx('foundation'))
console.log(example.isRx('foundation.persons.hari'))
// console.log('------\'foundation\', raw object ------ \n')
// // log(example)
// console.log('get raw nested property raw or with get returns the same')
// console.log(example.foundation.hari, example.get('foundation.hari'))
console.log('now change a reactive property directly like') // console.log('now change one of the nested properties and see if whole object foundation is reactive')
console.log('example.foundation.giskard = \'daneel a robot\'')
example.foundation.giskard = 'daneel a robot' // example.foundation.hari.planet = 'Trantor'
console.log('the new value extracted by the getter is',example.foundation.giskard) // example.set('foundation.hari.planet', 'Trantor')
// console.log(example.foundation)
// log(example)
// console.log('--------instance after doing rx----\n')
// console.log(example)
// console.log('--------------------------')
// console.log('now change a reactive property directly like')
// console.log('example.foundation.giskard = \'daneel a robot\'')
// example.foundation.giskard = 'daneel a robot'
// console.log('the new value extracted by the getter is', example.foundation.giskard)

View File

@ -31,8 +31,9 @@
}, },
"homepage": "https://github.com/uCOMmandIt/uci-utils#readme", "homepage": "https://github.com/uCOMmandIt/uci-utils#readme",
"dependencies": { "dependencies": {
"@uci-utils/bind-funcs": "^0.3.0",
"@uci-utils/logger": "^0.1.0", "@uci-utils/logger": "^0.1.0",
"@uci-utils/obj-nested-prop": "^0.1.1", "@uci-utils/obj-nested-prop": "^0.1.2",
"@uci-utils/type": "^0.6.2", "@uci-utils/type": "^0.6.2",
"deep-equal": "^2.0.5", "deep-equal": "^2.0.5",
"rxjs": "^7.0.0", "rxjs": "^7.0.0",

View File

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

View File

@ -1,4 +1,4 @@
import assert from 'assert' import assert, { doesNotMatch } from 'assert'
import { RxClass } from '../src/rx-class.js' import { RxClass } from '../src/rx-class.js'
const rxopts = Symbol.for('rx-class-opts') const rxopts = Symbol.for('rx-class-opts')
@ -8,6 +8,8 @@ const obj = {
today: { bing: 'bing', bong: { who: 'first', what: 'second' } } today: { bing: 'bing', bong: { who: 'first', what: 'second' } }
} }
const foundation = { planets: ['Trantor', 'Helcion'], persons: { giskard: { alias: 'daneel' }, hari: { name: 'seldon', planet: 'Helcion' } } }
class Test extends RxClass { class Test extends RxClass {
constructor (opts = {}) { constructor (opts = {}) {
super(opts) super(opts)
@ -17,121 +19,151 @@ class Test extends RxClass {
const opts = { const opts = {
[rxopts]: { [rxopts]: {
skip: 1, // handler: () => {}
handler: () => {} handler: (value) => { console.log('default subscription handler', value) }
} }
} }
const test = new Test(opts) const test = new Test(opts)
opts[rxopts].namespace = 'rx2'
opts[rxopts].getSetPrefix = '_'
opts[rxopts].getSetNamespace = '$'
const test2 = new Test(opts)
describe('Reactive Class', function () { describe('Reactive Class', function () {
it('Can be extended', function () { it('Can be extended', function () {
assert.deepStrictEqual(test.obj, obj) assert.deepStrictEqual(test.obj, obj)
}) })
it('Can get non rx deep value', function () { // it('Can get non rx deep value', function () {
assert.strictEqual(test.obj.today.bing, 'bing') // assert.strictEqual(test.obj.today.bing, 'bing')
assert.strictEqual(test.get('obj.today.bing'), 'bing') // assert.strictEqual(test.get('obj.today.bing'), 'bing')
assert(!test.isRx('obj.today.bing')) // assert(!test.isRx('obj.today.bing'))
}) // })
it('rx added to value', function () { // it('can make a property reactive', function () {
test.rxAdd('obj.today.bing') // test.rxAdd('obj.today.bing')
assert.strictEqual(test.obj.today.bing, 'bing') // assert(test.isRx('obj.today.bing'))
assert.strictEqual(test.get('obj.today.bing'), 'bing') // })
assert(test.isRx('obj.today.bing'))
it('can make a property containing object reactive', function () {
// test.foundation = foundation
test.set('foundation.test', foundation, { traverse: false })
test2.$._set('foundation.test', foundation, { traverse: false })
// console.log(test.foundation)
// test.rx.set('foundation.test.persons.jimbo', { name: 'jim', plant: 'remulak' })
// console.log(test.foundation.test)
assert(test.rx.isRx('foundation.test'))
assert(test2.rx2.isRx('foundation.test'))
// assert(test.rx.isRx('foundation.test.persons'))
// assert(test.rx.isRx('foundation.test.persons.giskard.alias'))
// assert(!test.rx.isRx('foundation.test.persons.bogus'))
}) })
}) })
describe('Emitted Events', function () { // describe('Emitted Events', function () {
function etest (event, value, path, done) { // function etest (event, value, path, done) {
test.once(event, (v, p) => { // test.once(event, (v, p) => {
// console.log('fired:', event, value, path) // // console.log('fired:', event, value, path)
assert.strictEqual(v, value) // assert.strictEqual(v, value)
assert.strictEqual(p, path) // assert.strictEqual(p, path)
clearTimeout(timeout) // clearTimeout(timeout)
done() // done()
}) // })
const timeout = setTimeout(() => { // const timeout = setTimeout(() => {
assert(false, `Event ${event} did not fire in 1000 ms.`) // assert(false, `Event ${event} did not fire in 1000 ms.`)
done() // done()
}, 1000) // }, 1000)
test.set(path, value) // test.set(path, value)
} // }
const path = 'obj.today.bing' // const path = 'obj.today.bing'
it('should emit global default event', function (done) { // it('should emit global default event', function (done) {
const event = 'changed' // const event = 'changed'
etest(event, event, path, done) // etest(event, event, path, done)
}) // })
it('should emit path if stringable', function (done) { // it('should emit path if stringable', function (done) {
const event = path // const event = path
etest(event, event, path, done) // etest(event, event, path, done)
}) // })
it('should emit the key name of value being changed', function (done) { // it('should emit the key name of value being changed', function (done) {
const event = path.split('.').pop() // const event = path.split('.').pop()
etest(event, event, path, done) // etest(event, event, path, done)
}) // })
it('should emit a custom set event for key/value change after force rxAdd', function (done) { // it('should emit a custom set event for key/value change after force rxAdd', function (done) {
const event = 'testing' // const event = 'testing'
test.rxAdd('obj.today.bing', { force: true, event: 'testing' }) // test.rxAdd('obj.today.bing', { force: true, event: 'testing' })
etest(event, event, path, done) // etest(event, event, path, done)
}) // })
}) // end emits // }) // end emits
describe('Subscriptions', function () { // describe('Subscriptions', function () {
const subsHandler = (value, timeout, name, done, v) => { // const subsHandler = (value, timeout, name, done, v) => {
assert.strictEqual(v, value) // assert.strictEqual(v, value)
clearTimeout(timeout) // clearTimeout(timeout)
test.rxRemoveSubs(name) // test.rxRemoveSubs(name)
done() // done()
} // }
function subs (name, path, value, done, handler) { // function subs (name, path, value, done, handler) {
const timeout = setTimeout(() => { // const timeout = setTimeout(() => {
assert(false, 'subscription handler did not react in 1000 ms.') // assert(false, 'subscription handler did not react in 1000 ms.')
done() // done()
}, 1000) // }, 1000)
const subscribe = handler ? { [handler]: subsHandler.bind(null, value, timeout, name, done) } : {} // const subscribe = handler ? { [handler]: subsHandler.bind(null, value, timeout, name, done) } : {}
test.rxAdd(path, { force: true, subscribe: subscribe }) // test.rxAdd(path, { force: true, subscribe: subscribe })
if (!handler) { // if (!handler) {
test.rxSubscribe('obj.today.bing', // test.rxSubscribe('obj.today.bing',
subsHandler.bind(null, value, timeout, name, done) // subsHandler.bind(null, value, timeout, name, done)
, name) // , name)
} // }
test.set(path, value) // test.set(path, value)
} // }
const path = 'obj.today.bing' // const path = 'obj.today.bing'
it('should react to default subscription', function (done) { // it('should react to default subscription', function (done) {
const name = '_default_' // const name = '_default_'
subs(name, path, name, done, name) // subs(name, path, name, done, name)
}) // })
it('should react to property/key subscription', function (done) { // it('should react to property/key subscription', function (done) {
const name = 'keytest' // const name = 'keytest'
subs(name, path, name, done, name) // subs(name, path, name, done, name)
}) // })
it('should react to new subscription', function (done) { // it('should react to new subscription', function (done) {
const name = 'atest' // const name = 'atest'
subs(name, path, name, done) // subs(name, path, name, done)
}) // })
}) // end subscriptions // }) // end subscriptions
describe('TODO: Amend,Hooks,Operators', function () { // describe('TODO: Amend,Hooks,Operators', function () {
it('should run a default hook', function (done) { // it('should run a default hook', function (done) {
done() // done()
}) // })
it('should run a custom hook', function (done) { // it('should run a custom hook', function (done) {
done() // done()
}) // })
it('should support custom rxjs operators', function (done) { // it('should support custom rxjs operators', function (done) {
done() // done()
}) // })
it('should amend a value that is set', function (done) { // it('should amend a value that is set', function (done) {
done() // done()
}) // })
}) // end hooks // }) // end hooks
// describe('TODO: Misc', function () {
// it('get should return values of object nested props', function (done) {
// done()
// })
// it('set should make all nested leaves reactive if traverse is requested', function (done) {
// done()
// })
// it('should remove rx for property', function (done) {
// done()
// })
// }) // end hooks