using new private methods and fields

added testing
using new obj-nested-prop module functions
master
Kebler Network System Administrator 2021-05-09 21:05:07 -07:00
parent d257ed459d
commit 543cad8bac
7 changed files with 427 additions and 203 deletions

10
.babelrc Normal file
View File

@ -0,0 +1,10 @@
{
"presets": [
[
"@babel/preset-env",
{
"shippedProposals": true
}
]
]
}

16
.eslintrc.yml Normal file
View File

@ -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"]

1
.npmrc Normal file
View File

@ -0,0 +1 @@
engine-strict=true

View File

@ -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"
} }
} }

View File

@ -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

View File

@ -1,93 +1,129 @@
import { EventEmitter } from 'events' // native modules
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 { subs: rx.subs[name], name: name }
}
}
return false
}
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]
return true
}
} else {
Object.values(rx.subs).forEach(sub => sub.unsubscribe())
rx.subs = {}
return true return true
} }
} }
return false return false
} }
rxUnsubscribe(rxObj,name){ // // takes several formats for a path to an objects property and return only the . string
if (typeof name !== 'string') return false // formatObjPath (path) {
const rx = (typeof rxObj === 'string' || Array.isArray(rxObj) ) ? this._rxGetObj(rxObj) : rxObj // if (path == null) return path
// if (name && name!=='_default' && (rxObj||{}).path) console.log('rx object for',rxObj.path) // if (Array.isArray(path)) {
if (rx.subs[name]){ // path = path.filter(Boolean).join('.') // Boolean filter strips empty strings or null/undefined
rx.subs[name].unsubscribe() // }
delete rx.subs[name] // path = path.replace(/\/:,/g, '.') // replaces /:, with .
// console.log(rx.path, ':current subscriptions>',Object.keys(rx.subs)) // return path
return true // }
}
return false
}
// takes several formats for a path to an objects property and return only the . string
formatObjPath(path) {
if (path==null) return path
if (Array.isArray(path)) {
path = path.filter(Boolean).join('.') // Boolean filter strips empty strings or null/undefined
}
path = path.replace(/\/:,/g, '.') // replaces /:, with .
return path
}
// checks for getter and the corresponding rx object // 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)
} }
} // end class
get(path){ // helpers
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={}){ export default RxClass
// console.log(path,value,opts,!this.isRx(path)) export { RxClass, set, get, del, getKeys, pathToString }
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

137
test/rxclass.test.js Normal file
View File

@ -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