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",
|
"name": "@uci-utils/rx-class",
|
||||||
"version": "0.1.5",
|
"version": "0.1.5",
|
||||||
"description": "class that support reactive properites",
|
"description": "class thats support reactive properites",
|
||||||
"main": "src/rx-class.js",
|
"main": "src/rx-class.js",
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"example": "node -r esm examples/example",
|
"example": "node examples/example",
|
||||||
"example:dev": "./node_modules/.bin/nodemon -r esm examples/example",
|
"example:dev": "./node_modules/.bin/nodemon examples/example",
|
||||||
"example:tra": "./node_modules/.bin/nodemon -r esm examples/traverse",
|
"example:dev:tra": "./node_modules/.bin/nodemon examples/traverse",
|
||||||
"test": "./node_modules/.bin/mocha -r esm --timeout 30000"
|
"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",
|
"author": "David Kebler",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -23,19 +31,21 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/uCOMmandIt/uci-utils#readme",
|
"homepage": "https://github.com/uCOMmandIt/uci-utils#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@uci-utils/set-value": "^3.0.3",
|
"@uci-utils/logger": "^0.1.0",
|
||||||
"deep-equal": "^2.0.3",
|
"@uci-utils/obj-nested-prop": "^0.1.1",
|
||||||
"get-value": "^3.0.1",
|
"@uci-utils/type": "^0.6.2",
|
||||||
"is-plain-obj": "^2.1.0",
|
"deep-equal": "^2.0.5",
|
||||||
"is-plain-object": "^3.0.0",
|
"rxjs": "^7.0.0",
|
||||||
"rxjs": "^6.5.5",
|
"traverse": "^0.6.6"
|
||||||
"traverse": "^0.6.6",
|
|
||||||
"unset-value": "^1.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.2.0",
|
"@babel/core": "^7.14.0",
|
||||||
"esm": "^3.2.25",
|
"@babel/eslint-parser": "^7.13.14",
|
||||||
"mocha": "^7.2.0",
|
"@babel/preset-env": "^7.14.1",
|
||||||
"nodemon": "^2.0.4"
|
"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.
|
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?
|
## Why?
|
||||||
|
|
||||||
To have reactivity similar to vue and react but in any backend object?
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
## Features
|
## 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
|
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
|
||||||
|
|
384
src/rx-class.js
384
src/rx-class.js
|
@ -1,93 +1,129 @@
|
||||||
|
// native modules
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
|
// reactive modules
|
||||||
import { BehaviorSubject, from } from 'rxjs'
|
import { BehaviorSubject, from } from 'rxjs'
|
||||||
import { distinctUntilChanged, skip} from 'rxjs/operators'
|
import { distinctUntilChanged, skip } from 'rxjs/operators'
|
||||||
// ** nested object access **
|
// uci modules
|
||||||
import _get from 'get-value'
|
import { check } from '@uci-utils/type'
|
||||||
import _set from '@uci-utils/set-value'
|
import { get, set, del, getKeys, pathToString } from '@uci-utils/obj-nested-prop'
|
||||||
import _del from 'unset-value'
|
import { logger } from '@uci-utils/logger'
|
||||||
// *********************
|
// supporting
|
||||||
import equal from 'deep-equal'
|
import equal from 'deep-equal'
|
||||||
import traverse from 'traverse'
|
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' })
|
||||||
constructor(opts={}){
|
|
||||||
|
// to pass rx options use 'Symbol.for('rx-class-opts')'
|
||||||
|
class RxClass extends EventEmitter {
|
||||||
|
// private fields
|
||||||
|
#rx
|
||||||
|
#rxM
|
||||||
|
|
||||||
|
constructor (opts = {}) {
|
||||||
super(opts)
|
super(opts)
|
||||||
const rxopts = opts._rx_||{}
|
const rxopts = opts[Symbol.for('rx-class-opts')] || {}
|
||||||
this._rx_ = {
|
this.#rx = {
|
||||||
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,
|
handler: rxopts.handler, // default subscription handler
|
||||||
props: {},
|
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
|
||||||
hooks: [], // will add to all setters
|
hook: rxopts.hook, // function called when setting
|
||||||
root: '', // root path/namespace to added to all get/set paths
|
namespace: rxopts.namespace || 'rx', // TODO namespace to added to all public rx methods
|
||||||
operators:[], // will add to all pipes
|
operators: new Map(), // TODO added to all rx pipes, in order
|
||||||
skip: rxopts.skip == null ? 0 : rxopts.skip
|
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
|
get (path) {
|
||||||
_rxGetObj(path,prop) {
|
const value = this.#get(path)
|
||||||
path = this.formatObjPath(path)
|
if (!check.isPlainObject(value)) return value
|
||||||
path = path.split('.')
|
const obj = {} // return a copy with actual values instead of getters
|
||||||
const name = path.pop()
|
const self = this
|
||||||
path = path.join('.')
|
traverse(this.#get(path)).map(function () {
|
||||||
let parent = path ? this.$get(path) : this // if path is empty string return this
|
if (this.isLeaf) {
|
||||||
// console.log(name,path,parent)
|
self.#set(obj, this.path, this.node)
|
||||||
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 obj
|
||||||
return prop ? (rx || {})[prop] : rx
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if property exists it moves value to _ version and creates getter and setter and behavior subject
|
set (path, value, opts = {}) {
|
||||||
rxAdd(opath,opts={}) {
|
// console.log(path,value,opts,!this.isRx(path))
|
||||||
const path = this.formatObjPath(opath)
|
if (!opts.noRx && !this.isRx(path)) {
|
||||||
if (opts.traverse) {
|
opts = Object.assign(check.isPlainObject(value) ? { values: value, traverse: true } : { value: value }, opts)
|
||||||
const obj = opts.values ? opts.values : this.$get(path)
|
// 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
|
const self = this
|
||||||
// console.log('object to traverse', obj)
|
// console.log('object to traverse', obj)
|
||||||
traverse(obj).map(function () {
|
traverse(obj).map(function () {
|
||||||
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.rxAdd(lpath, { value: this.node })
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
} // end traverse
|
} // end traverse
|
||||||
if (this.isRx(path)) return true
|
if (this.isRx(path) && !opts.force) return true
|
||||||
const value = this.$get(path)
|
const value = this.#get(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)
|
this.#set(path, null)
|
||||||
}
|
}
|
||||||
const {parent,name} = this._rxGetObj(path,'__parent__')
|
const { parent, name } = this.#rxGetObj(path, '__parent__')
|
||||||
this.$set(['_rx_.props',path],{new:true})
|
log.debug({
|
||||||
let rx = this.$get(this.formatObjPath(['_rx_.props',path]))
|
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)
|
// 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.#rx.amendValue || ((val) => val)
|
||||||
if (opts.amend_path || this._rx_.amend_path) rx.amendValue = (value,path) => {return {value:value, path: path}}
|
rx.obs = from(new BehaviorSubject(rx.amendValue(rx.value, rx.path)).pipe(
|
||||||
// console.log(path,': initial value===>',rx.amendValue(rx.value,rx.path))
|
skip(opts.skip != null ? opts.skip : this.#rx.skip),
|
||||||
rx.obs = from(new BehaviorSubject(rx.amendValue(rx.value,rx.path)).pipe(
|
distinctUntilChanged() // allow custom comparator
|
||||||
skip(opts.skip != null ? opts.skip : this._rx_.skip),
|
// allow custom/additional operators here
|
||||||
distinctUntilChanged()
|
// 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.hooks = []
|
rx.hook = opts.hook
|
||||||
this.rxSubscribe(rx,'_default',this._rx_.handler,)
|
if (check.isFunction(this.#rx.handler)) this.rxSubscribe(rx, this.#rx.handler, '_default_')
|
||||||
this.rxSubscribe(rx,(opts.subscribe || {}).name,(opts.subscribe ||{}).handler)
|
const subs = Object.entries(opts.subscribe || {})
|
||||||
|
subs.forEach(sub => {
|
||||||
|
if (check.isFunction(sub[1])) this.rxSubscribe(rx, sub[1], sub[0])
|
||||||
|
})
|
||||||
const self = this
|
const self = this
|
||||||
Object.defineProperty(parent, name, {
|
Object.defineProperty(parent, name, {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
|
@ -96,67 +132,68 @@ export default class RxClass extends EventEmitter {
|
||||||
return rx.value
|
return rx.value
|
||||||
},
|
},
|
||||||
set (value) {
|
set (value) {
|
||||||
// console.log('in setter', path,value)
|
|
||||||
rx.value = value
|
rx.value = value
|
||||||
value = rx.amendValue(value,path)
|
value = rx.amendValue(value, path)
|
||||||
rx.obs.next(value)
|
rx.obs.next(value) // react
|
||||||
if (self._rx_.emitter) {
|
if (self.#rx.emitter) { // emit
|
||||||
if (opts.event) self.emit(opts.event,value)
|
if (opts.event) self.emit(opts.event, value, path) // custom event
|
||||||
if (self._rx_.event) self.emit(self._rx_.event,value,path)
|
if (self.#rx.event) self.emit(self.#rx.event, value, path) // global event
|
||||||
self.emit(path,value)
|
const spath = pathToString(path)
|
||||||
self.emit(name,value,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
|
// any hook function that is already bound will use that context, otherwise this
|
||||||
self._rx_.hooks.forEach(hook=>hook.call(self,value))
|
// hooks
|
||||||
rx.hooks.forEach(hook=>hook.call(self,value))
|
// 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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// if name is only passed remove
|
// if name is only passed remove
|
||||||
rxHook(func,name) {
|
// rxHook (func, path) {
|
||||||
this._rx_.hooks[name]=func
|
// this.#rx.hooks[name] = func
|
||||||
}
|
// }
|
||||||
|
|
||||||
rxAmendValue(path,func) {
|
rxAmendValue (path, func) {
|
||||||
let rx = {}
|
if (arguments.length === 0) { this.#rx.amendValue = null; return }
|
||||||
if (arguments.length === 0 ) {this._rx_.amendValue= null;return}
|
if (typeof path === 'function') { this.#rx.amendValue = path; return }
|
||||||
if (typeof path === 'function') {this._rx_.amendValue=path;return}
|
const rx = this.#rxGetObj(path)
|
||||||
rx = (typeof path === 'string') ? this._rxGetObj(path) : path
|
if (!check.isEmptyPlainObject(rx)) return false
|
||||||
if (!rx) return false
|
func = func || (val => val)
|
||||||
func = func ? func : (val => val )
|
rx.amendValue = func === 'path' ? (value, path) => { return { value: value, path: path } } : func
|
||||||
console.log('amend', 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) {
|
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
|
// removes obser, subscriptions, and getter and setter and make back into plain value
|
||||||
rxRemove(path,opts={}) {
|
rxRemove (path, opts = {}) {
|
||||||
if (!opts.confirm) return false // must confirm to remove
|
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
|
const self = this
|
||||||
traverse(this.$get(path)).map(function () {
|
traverse(this.#get(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.isRx(lpath)) self.rxRemove(lpath, { confirm: true })
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
// all done removing the leaves so remove branch
|
// all done removing the leaves so remove branch
|
||||||
this.$del(['_rx_.props',path],true)
|
this.#del(['#rx.props', path], true)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
let { parent, name, parentPath } = this._rxGetObj(path,'__parent__')
|
const { parent, name, parentPath } = this.#rxGetObj(path, '__parent__')
|
||||||
const rxparent = this.$get(['_rx_.props',parentPath])
|
const rxparent = this.#get(['#rx.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())
|
||||||
delete parent[name]
|
delete parent[name]
|
||||||
parent[name] = rx.value
|
parent[name] = rx.value
|
||||||
delete rxparent[name]
|
delete rxparent[name]
|
||||||
|
@ -164,114 +201,119 @@ export default class RxClass extends EventEmitter {
|
||||||
return true
|
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
|
// pass rx as object or path
|
||||||
rxSubscribe(rxObj,name,handler){
|
rxSubscribe (path, handler, name) {
|
||||||
// if (name ==='ha') {
|
const rx = this.#rxGetObj(path)
|
||||||
// console.log(this.id,'making subscription',name,rxObj.path || rxObj,!!handler)
|
if (rx) {
|
||||||
// }
|
// TODO if no name generate a name and return with handle
|
||||||
if (typeof name !== 'string') return false
|
if (check.isFunction(handler)) {
|
||||||
const rx = (typeof rxObj === 'string' || Array.isArray(rxObj) ) ? this._rxGetObj(rxObj) : rxObj
|
if (rx.subs[name]) rx.subs[name].unsubscribe() // replace if exits
|
||||||
// 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)
|
rx.subs[name] = rx.obs.subscribe(handler)
|
||||||
// console.log(rx.path, 'current subscriptions---',Object.keys(rx.subs))
|
// console.log(rx.path, 'current subscriptions---', Object.keys(rx.subs))
|
||||||
return true
|
return { subs: rx.subs[name], name: name }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
rxUnsubscribe(rxObj,name){
|
rxRemoveSubs (path, name) {
|
||||||
if (typeof name !== 'string') return false
|
const rx = this.#rxGetObj(path)
|
||||||
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 (name && name!=='_default' && (rxObj||{}).path) console.log('rx object for',rxObj.path)
|
||||||
if (rx.subs[name]){
|
if (rx) {
|
||||||
|
if (name) {
|
||||||
|
if (rx.subs[name]) {
|
||||||
rx.subs[name].unsubscribe()
|
rx.subs[name].unsubscribe()
|
||||||
delete rx.subs[name]
|
delete rx.subs[name]
|
||||||
// console.log(rx.path, ':current subscriptions>',Object.keys(rx.subs))
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Object.values(rx.subs).forEach(sub => sub.unsubscribe())
|
||||||
|
rx.subs = {}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// takes several formats for a path to an objects property and return only the . string
|
// // takes several formats for a path to an objects property and return only the . string
|
||||||
formatObjPath(path) {
|
// formatObjPath (path) {
|
||||||
if (path==null) return path
|
// if (path == null) return path
|
||||||
if (Array.isArray(path)) {
|
// if (Array.isArray(path)) {
|
||||||
path = path.filter(Boolean).join('.') // Boolean filter strips empty strings or null/undefined
|
// path = path.filter(Boolean).join('.') // Boolean filter strips empty strings or null/undefined
|
||||||
}
|
// }
|
||||||
path = path.replace(/\/:,/g, '.') // replaces /:, with .
|
// path = path.replace(/\/:,/g, '.') // replaces /:, with .
|
||||||
return path
|
// return path
|
||||||
}
|
// }
|
||||||
|
|
||||||
// checks for getter and the corresponding rx object
|
// checks for getter and the corresponding rx object
|
||||||
isRx(path) {
|
isRx (path) {
|
||||||
if (this.$get(path)===undefined) return false
|
// console.log(path)
|
||||||
path = this.formatObjPath(path)
|
if (this.#get(path) === undefined) return false
|
||||||
const { parent, name, parentPath } = this._rxGetObj(path,'__parent__')
|
const { parent, name, parentPath } = this.#rxGetObj(path, '__parent__')
|
||||||
const rxparent = this.$get(['_rx_.props',parentPath]) || {}
|
// console.log(parent, name, parentPath)
|
||||||
|
const rxparent = this.#get(['#rx.props', parentPath]) || {}
|
||||||
const rx = rxparent[name]
|
const rx = rxparent[name]
|
||||||
// console.log (path, 'rxparent,rx', !!parent, !!name, !!rxparent, !!rx)
|
// console.log(path, 'rxparent,rx', !!parent, !!name, !!rxparent, !!rx)
|
||||||
if (!rx) return false
|
if (!rx) return false
|
||||||
// console.log(Object.getOwnPropertyDescriptor(parent, name)['get'] )
|
// console.log(Object.getOwnPropertyDescriptor(parent, name).get)
|
||||||
return !!Object.getOwnPropertyDescriptor(parent, name)['get']
|
return !!Object.getOwnPropertyDescriptor(parent, name).get
|
||||||
}
|
}
|
||||||
|
|
||||||
$set () {
|
// PRIVATE METHODS
|
||||||
let args = [...arguments]
|
|
||||||
|
// 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) 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
|
||||||
args[1] = this.formatObjPath(args[1])
|
// console.log('in #set',args[1],args[2])
|
||||||
// console.log('in $set',args[1],args[2])
|
return set(...args)
|
||||||
return _set(...args)
|
|
||||||
}
|
}
|
||||||
$get () {
|
|
||||||
let args = [...arguments]
|
#get () {
|
||||||
|
const args = [...arguments]
|
||||||
if (args.length === 1) args.unshift(this)
|
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.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)
|
||||||
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
|
} // 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