using new private methods and fields
added testing using new obj-nested-prop module functionsmaster
parent
d257ed459d
commit
543cad8bac
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"shippedProposals": true
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
env:
|
||||
node: true
|
||||
es2021: true
|
||||
mocha: true
|
||||
extends:
|
||||
- standard
|
||||
parser: "@babel/eslint-parser"
|
||||
parserOptions:
|
||||
ecmaVersion: 12
|
||||
sourceType: module
|
||||
rules:
|
||||
indent: ["error", 2]
|
||||
no-console: 0
|
||||
semi: ["error", "never"]
|
||||
# linebreak-style: ["error", "unix"]
|
||||
quotes: ["error", "single"]
|
44
package.json
44
package.json
|
@ -1,13 +1,21 @@
|
|||
{
|
||||
"name": "@uci-utils/rx-class",
|
||||
"version": "0.1.5",
|
||||
"description": "class that support reactive properites",
|
||||
"description": "class thats support reactive properites",
|
||||
"main": "src/rx-class.js",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"scripts": {
|
||||
"example": "node -r esm examples/example",
|
||||
"example:dev": "./node_modules/.bin/nodemon -r esm examples/example",
|
||||
"example:tra": "./node_modules/.bin/nodemon -r esm examples/traverse",
|
||||
"test": "./node_modules/.bin/mocha -r esm --timeout 30000"
|
||||
"example": "node examples/example",
|
||||
"example:dev": "./node_modules/.bin/nodemon examples/example",
|
||||
"example:dev:tra": "./node_modules/.bin/nodemon examples/traverse",
|
||||
"test": "./node_modules/.bin/mocha --timeout 30000",
|
||||
"test:dev": "UCI_ENV=dev UCI_LOG_PRETTY='verbose' ./node_modules/.bin/nodemon --exec './node_modules/.bin/mocha --timeout 30000' || exit 0",
|
||||
"test:dev:trace": "UCI_LOG_LEVEL='trace' npm run test:dev",
|
||||
"test:dev:debug": "UCI_LOG_LEVEL='debug' npm run test:dev",
|
||||
"test:dev:error": "UCI_LOG_LEVEL='error' npm run test:dev"
|
||||
},
|
||||
"author": "David Kebler",
|
||||
"license": "MIT",
|
||||
|
@ -23,19 +31,21 @@
|
|||
},
|
||||
"homepage": "https://github.com/uCOMmandIt/uci-utils#readme",
|
||||
"dependencies": {
|
||||
"@uci-utils/set-value": "^3.0.3",
|
||||
"deep-equal": "^2.0.3",
|
||||
"get-value": "^3.0.1",
|
||||
"is-plain-obj": "^2.1.0",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"rxjs": "^6.5.5",
|
||||
"traverse": "^0.6.6",
|
||||
"unset-value": "^1.0.0"
|
||||
"@uci-utils/logger": "^0.1.0",
|
||||
"@uci-utils/obj-nested-prop": "^0.1.1",
|
||||
"@uci-utils/type": "^0.6.2",
|
||||
"deep-equal": "^2.0.5",
|
||||
"rxjs": "^7.0.0",
|
||||
"traverse": "^0.6.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.2.0",
|
||||
"esm": "^3.2.25",
|
||||
"mocha": "^7.2.0",
|
||||
"nodemon": "^2.0.4"
|
||||
"@babel/core": "^7.14.0",
|
||||
"@babel/eslint-parser": "^7.13.14",
|
||||
"@babel/preset-env": "^7.14.1",
|
||||
"chai": "^4.3.4",
|
||||
"eslint": "^7.26.0",
|
||||
"eslint-config-standard": "^16.0.2",
|
||||
"mocha": "^8.4.0",
|
||||
"nodemon": "^2.0.7"
|
||||
}
|
||||
}
|
||||
|
|
18
readme.md
18
readme.md
|
@ -2,17 +2,25 @@
|
|||
|
||||
A Great Class to Extend! It has built in support to make reactive any/all class properties and their nested leaves.
|
||||
|
||||
## ES2021 Dependency
|
||||
|
||||
Not running nodejs 16+ which supports much of ES2021+? then STOP you can't use this class! It contains both private fields and methods and is written with esm modules. You might be able to transpile it but that is not supported nor encouraged.
|
||||
|
||||
|
||||
## Why?
|
||||
|
||||
To have reactivity similar to vue and react but in any backend object?
|
||||
This allow a backend app to maintain app settings and allow "clients" to get updated values when the app changes state. Essentially will create a 'mini' state event bus.
|
||||
To allow and object to have reactivity similar to vue and react without those packages and thus with any nodejs code. This a "backend" app could maintain app settings and allow non vue/react "clients" to get updated values when the app changes state. Essentially this class will create instances of 'mini' state event bus.
|
||||
|
||||
## Features
|
||||
|
||||
Will both emit and allow subscriptions to any property 'path' that has been made reactive.
|
||||
Will both emit and allow subscriptions to any property (i.e. object path) that has been made reactive.
|
||||
|
||||
Allows custom registration of unlimited state change event hooks.
|
||||
Allows custom registration of an unlimited number of state change event hooks.
|
||||
|
||||
Allows additional rxjs operators for the pipe the observer for each property.
|
||||
Allows additional rxjs operators for the pipe of the observer for each property.
|
||||
|
||||
See Examples folder
|
||||
|
||||
# How
|
||||
|
||||
The class maintain a private `rx` field. When an object (i.e. property) of the class is made reactive it is converted into a getter/setter (much like vue) and it's actual value is stored within the private rx field. One can access the value of that propery like before but
|
||||
|
|
346
src/rx-class.js
346
src/rx-class.js
|
@ -1,93 +1,129 @@
|
|||
// native modules
|
||||
import { EventEmitter } from 'events'
|
||||
// reactive modules
|
||||
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'
|
||||
// *********************
|
||||
// uci modules
|
||||
import { check } from '@uci-utils/type'
|
||||
import { get, set, del, getKeys, pathToString } from '@uci-utils/obj-nested-prop'
|
||||
import { logger } from '@uci-utils/logger'
|
||||
// supporting
|
||||
import equal from 'deep-equal'
|
||||
import traverse from 'traverse'
|
||||
import isPlainObject from 'is-plain-object'
|
||||
|
||||
export default class RxClass extends EventEmitter {
|
||||
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 {
|
||||
// private fields
|
||||
#rx
|
||||
#rxM
|
||||
|
||||
constructor (opts = {}) {
|
||||
super(opts)
|
||||
const rxopts = opts._rx_||{}
|
||||
this._rx_ = {
|
||||
const rxopts = opts[Symbol.for('rx-class-opts')] || {}
|
||||
this.#rx = {
|
||||
emitter: rxopts.emitter != null ? rxopts.emitter : true,
|
||||
event: rxopts.event === false ? false : rxopts.event || 'changed',
|
||||
handler: rxopts.handler,
|
||||
props: {},
|
||||
handler: rxopts.handler, // default subscription handler
|
||||
props: {}, // where reactive property info is store including actual values
|
||||
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
|
||||
// amend_path: rxopts.amend_path, // by default amend value with path
|
||||
hook: rxopts.hook, // function called when setting
|
||||
namespace: rxopts.namespace || 'rx', // TODO namespace to added to all public rx methods
|
||||
operators: new Map(), // TODO added to all rx pipes, in order
|
||||
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
|
||||
|
||||
get (path) {
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
set (path, value, opts = {}) {
|
||||
// console.log(path,value,opts,!this.isRx(path))
|
||||
if (!opts.noRx && !this.isRx(path)) {
|
||||
opts = Object.assign(check.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)
|
||||
}
|
||||
}
|
||||
|
||||
// getRaw () {
|
||||
// return get(this, [this.#rx.namespace, ...arguments])
|
||||
// }
|
||||
|
||||
// if property exists it moves value to #rx.props and creates getter and setter and behavior subject
|
||||
rxAdd (path, opts = {}) {
|
||||
if (opts.traverse) { // will add rx to all leaves
|
||||
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}`)
|
||||
// console.log(`#{!opts.values ? 'existing':'new'} leaf on path '#{lpath}' with value: #{this.node}`)
|
||||
self.rxAdd(lpath, { value: this.node })
|
||||
}
|
||||
return true
|
||||
}
|
||||
)
|
||||
return true
|
||||
} // end traverse
|
||||
if (this.isRx(path)) return true
|
||||
const value = this.$get(path)
|
||||
if (this.isRx(path) && !opts.force) return true
|
||||
const value = this.#get(path) // current value
|
||||
if (value === undefined) {
|
||||
// console.log ('no current property or value for',path,'creating temporary null')
|
||||
this.$set(path,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]))
|
||||
|
||||
const { parent, name } = this.#rxGetObj(path, '__parent__')
|
||||
log.debug({
|
||||
class: 'RxClass',
|
||||
method: '#rxGetObj',
|
||||
path: path,
|
||||
name: name,
|
||||
parent: parent,
|
||||
msg: 'got name and parent'
|
||||
})
|
||||
this.#set(['#rx.props', path], {})
|
||||
const rx = this.#get(['#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.amendValue = opts.amendValue || this.#rx.amendValue || ((val) => val)
|
||||
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))
|
||||
skip(opts.skip != null ? opts.skip : this.#rx.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.hooks = []
|
||||
this.rxSubscribe(rx,'_default',this._rx_.handler,)
|
||||
this.rxSubscribe(rx,(opts.subscribe || {}).name,(opts.subscribe ||{}).handler)
|
||||
rx.hook = opts.hook
|
||||
if (check.isFunction(this.#rx.handler)) this.rxSubscribe(rx, this.#rx.handler, '_default_')
|
||||
const subs = Object.entries(opts.subscribe || {})
|
||||
subs.forEach(sub => {
|
||||
if (check.isFunction(sub[1])) this.rxSubscribe(rx, sub[1], sub[0])
|
||||
})
|
||||
const self = this
|
||||
Object.defineProperty(parent, name, {
|
||||
configurable: true,
|
||||
|
@ -96,64 +132,65 @@ export default class RxClass extends EventEmitter {
|
|||
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)
|
||||
rx.obs.next(value) // react
|
||||
if (self.#rx.emitter) { // emit
|
||||
if (opts.event) self.emit(opts.event, value, path) // custom event
|
||||
if (self.#rx.event) self.emit(self.#rx.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 already bound will use that context
|
||||
self._rx_.hooks.forEach(hook=>hook.call(self,value))
|
||||
rx.hooks.forEach(hook=>hook.call(self,value))
|
||||
// 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.#rx.hook)) self.#rx.hook.call(self, value) // global hook
|
||||
if (check.isFunction(rx.hook)) rx.hook.call(self, value) // property hook
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// if name is only passed remove
|
||||
rxHook(func,name) {
|
||||
this._rx_.hooks[name]=func
|
||||
}
|
||||
// rxHook (func, path) {
|
||||
// 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)
|
||||
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
|
||||
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
|
||||
rxOperator (func, name) {
|
||||
this._rx_.operators[name]=func
|
||||
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))) {
|
||||
if (!this.isRx(path) && check.isPlainObject(this.#get(path))) {
|
||||
const self = this
|
||||
traverse(this.$get(path)).map(function () {
|
||||
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 })
|
||||
}
|
||||
return true
|
||||
})
|
||||
// all done removing the leaves so remove branch
|
||||
this.$del(['_rx_.props',path],true)
|
||||
this.#del(['#rx.props', path], true)
|
||||
return true
|
||||
}
|
||||
let { parent, name, parentPath } = this._rxGetObj(path,'__parent__')
|
||||
const rxparent = this.$get(['_rx_.props',parentPath])
|
||||
const { 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())
|
||||
|
@ -164,114 +201,119 @@ export default class RxClass extends EventEmitter {
|
|||
return true
|
||||
}
|
||||
|
||||
rxGetObs (path) {
|
||||
return (this.#rxGetObj(path) || {}).obs
|
||||
}
|
||||
|
||||
rxGetSubs (path, name) {
|
||||
const rx = this.#rxGetObj(path)
|
||||
if (rx) return (name ? rx.subs[name] : Object.keys(rx.subs))
|
||||
}
|
||||
|
||||
// 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') {
|
||||
rxSubscribe (path, handler, name) {
|
||||
const rx = this.#rxGetObj(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 true
|
||||
return { subs: rx.subs[name], name: name }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
rxUnsubscribe(rxObj,name){
|
||||
if (typeof name !== 'string') return false
|
||||
const rx = (typeof rxObj === 'string' || Array.isArray(rxObj) ) ? this._rxGetObj(rxObj) : rxObj
|
||||
rxRemoveSubs (path, name) {
|
||||
const rx = this.#rxGetObj(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]
|
||||
// console.log(rx.path, ':current subscriptions>',Object.keys(rx.subs))
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
Object.values(rx.subs).forEach(sub => sub.unsubscribe())
|
||||
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
|
||||
}
|
||||
// // 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]) || {}
|
||||
// console.log(path)
|
||||
if (this.#get(path) === undefined) return false
|
||||
const { parent, name, parentPath } = this.#rxGetObj(path, '__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 false
|
||||
// console.log(Object.getOwnPropertyDescriptor(parent, name)['get'] )
|
||||
return !!Object.getOwnPropertyDescriptor(parent, name)['get']
|
||||
// console.log(Object.getOwnPropertyDescriptor(parent, name).get)
|
||||
return !!Object.getOwnPropertyDescriptor(parent, name).get
|
||||
}
|
||||
|
||||
$set () {
|
||||
let args = [...arguments]
|
||||
// PRIVATE METHODS
|
||||
|
||||
// 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) {
|
||||
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 = this.#get(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 = this.#get(['#rx.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 () {
|
||||
const 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)
|
||||
// console.log('in #set',args[1],args[2])
|
||||
return set(...args)
|
||||
}
|
||||
$get () {
|
||||
let args = [...arguments]
|
||||
|
||||
#get () {
|
||||
const args = [...arguments]
|
||||
if (args.length === 1) args.unshift(this)
|
||||
args[1] = this.formatObjPath(args[1])
|
||||
return _get(...args)
|
||||
return get(...args)
|
||||
}
|
||||
$del () {
|
||||
let args = [...arguments]
|
||||
|
||||
#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)
|
||||
args[1] = this.formatObjPath(args[1])
|
||||
return _del(...args)
|
||||
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
|
||||
|
||||
// helpers
|
||||
|
||||
export default RxClass
|
||||
export { RxClass, set, get, del, getKeys, pathToString }
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
import assert from 'assert'
|
||||
import { RxClass } from '../src/rx-class.js'
|
||||
|
||||
const rxopts = Symbol.for('rx-class-opts')
|
||||
|
||||
const obj = {
|
||||
tomorrow: { bar: 'fight', foo: 'fighters' },
|
||||
today: { bing: 'bing', bong: { who: 'first', what: 'second' } }
|
||||
}
|
||||
|
||||
class Test extends RxClass {
|
||||
constructor (opts = {}) {
|
||||
super(opts)
|
||||
this.obj = obj
|
||||
}
|
||||
}
|
||||
|
||||
const opts = {
|
||||
[rxopts]: {
|
||||
skip: 1,
|
||||
handler: () => {}
|
||||
}
|
||||
}
|
||||
|
||||
const test = new Test(opts)
|
||||
|
||||
describe('Reactive Class', function () {
|
||||
it('Can be extended', function () {
|
||||
assert.deepStrictEqual(test.obj, obj)
|
||||
})
|
||||
|
||||
it('Can get non rx deep value', function () {
|
||||
assert.strictEqual(test.obj.today.bing, 'bing')
|
||||
assert.strictEqual(test.get('obj.today.bing'), 'bing')
|
||||
assert(!test.isRx('obj.today.bing'))
|
||||
})
|
||||
|
||||
it('rx added to value', function () {
|
||||
test.rxAdd('obj.today.bing')
|
||||
assert.strictEqual(test.obj.today.bing, 'bing')
|
||||
assert.strictEqual(test.get('obj.today.bing'), 'bing')
|
||||
assert(test.isRx('obj.today.bing'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Emitted Events', function () {
|
||||
function etest (event, value, path, done) {
|
||||
test.once(event, (v, p) => {
|
||||
// console.log('fired:', event, value, path)
|
||||
assert.strictEqual(v, value)
|
||||
assert.strictEqual(p, path)
|
||||
clearTimeout(timeout)
|
||||
done()
|
||||
})
|
||||
const timeout = setTimeout(() => {
|
||||
assert(false, `Event ${event} did not fire in 1000 ms.`)
|
||||
done()
|
||||
}, 1000)
|
||||
test.set(path, value)
|
||||
}
|
||||
|
||||
const path = 'obj.today.bing'
|
||||
it('should emit global default event', function (done) {
|
||||
const event = 'changed'
|
||||
etest(event, event, path, done)
|
||||
})
|
||||
it('should emit path if stringable', function (done) {
|
||||
const event = path
|
||||
etest(event, event, path, done)
|
||||
})
|
||||
|
||||
it('should emit the key name of value being changed', function (done) {
|
||||
const event = path.split('.').pop()
|
||||
etest(event, event, path, done)
|
||||
})
|
||||
|
||||
it('should emit a custom set event for key/value change after force rxAdd', function (done) {
|
||||
const event = 'testing'
|
||||
test.rxAdd('obj.today.bing', { force: true, event: 'testing' })
|
||||
etest(event, event, path, done)
|
||||
})
|
||||
}) // end emits
|
||||
|
||||
describe('Subscriptions', function () {
|
||||
const subsHandler = (value, timeout, name, done, v) => {
|
||||
assert.strictEqual(v, value)
|
||||
clearTimeout(timeout)
|
||||
test.rxRemoveSubs(name)
|
||||
done()
|
||||
}
|
||||
function subs (name, path, value, done, handler) {
|
||||
const timeout = setTimeout(() => {
|
||||
assert(false, 'subscription handler did not react in 1000 ms.')
|
||||
done()
|
||||
}, 1000)
|
||||
const subscribe = handler ? { [handler]: subsHandler.bind(null, value, timeout, name, done) } : {}
|
||||
test.rxAdd(path, { force: true, subscribe: subscribe })
|
||||
if (!handler) {
|
||||
test.rxSubscribe('obj.today.bing',
|
||||
subsHandler.bind(null, value, timeout, name, done)
|
||||
, name)
|
||||
}
|
||||
test.set(path, value)
|
||||
}
|
||||
|
||||
const path = 'obj.today.bing'
|
||||
it('should react to default subscription', function (done) {
|
||||
const name = '_default_'
|
||||
subs(name, path, name, done, name)
|
||||
})
|
||||
it('should react to property/key subscription', function (done) {
|
||||
const name = 'keytest'
|
||||
subs(name, path, name, done, name)
|
||||
})
|
||||
it('should react to new subscription', function (done) {
|
||||
const name = 'atest'
|
||||
subs(name, path, name, done)
|
||||
})
|
||||
}) // end subscriptions
|
||||
|
||||
describe('TODO: Amend,Hooks,Operators', function () {
|
||||
it('should run a default hook', function (done) {
|
||||
done()
|
||||
})
|
||||
|
||||
it('should run a custom hook', function (done) {
|
||||
done()
|
||||
})
|
||||
|
||||
it('should support custom rxjs operators', function (done) {
|
||||
done()
|
||||
})
|
||||
|
||||
it('should amend a value that is set', function (done) {
|
||||
done()
|
||||
})
|
||||
}) // end hooks
|
Loading…
Reference in New Issue