A working class, only missing the tested hook and operator support
parent
f1e4dac3a0
commit
47e4c34ef5
|
@ -0,0 +1,37 @@
|
|||
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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
/node_modules/
|
||||
/coverage/
|
|
@ -0,0 +1,5 @@
|
|||
tests/
|
||||
test/
|
||||
*.test.js
|
||||
testing/
|
||||
examples/
|
|
@ -0,0 +1,63 @@
|
|||
import RxClass from '../src/rx-class.js'
|
||||
|
||||
class Example extends RxClass {
|
||||
constructor(opts={}){
|
||||
super(opts)
|
||||
this.test = 10
|
||||
this.another = { bar:'bust', foo:3}
|
||||
}
|
||||
}
|
||||
|
||||
const example = new Example({
|
||||
_rx_ : {
|
||||
handler: value => {console.log('-----------default subscription handler-------', value)}
|
||||
}
|
||||
})
|
||||
|
||||
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))
|
||||
example.on('test', (val) => console.log('emitted value of test',val))
|
||||
|
||||
console.log('making \'test\' reactive')
|
||||
example.rxAdd('test')
|
||||
console.log('changing test to 20, 30 directly')
|
||||
example.test = 20
|
||||
example.test = 30
|
||||
example.rxSubscribe('test',(prop)=>console.log('#############late subscription to \'test\' after 30 ############',prop),'custom')
|
||||
console.log('changing test to 40 via setPath')
|
||||
example.setPath('test',40)
|
||||
|
||||
console.log('making a deeper property \'another.foo\' reactive')
|
||||
example.rxAdd('another.foo')
|
||||
|
||||
example.another.foo = 7
|
||||
|
||||
console.log('direct access of another.foo',example.another.foo)
|
||||
console.log('access via get of another.foo',example.getPath('another.foo'))
|
||||
|
||||
console.log('making a deeper property \'some.thing\' reactive that did not exist')
|
||||
example.rxAdd('some.thing')
|
||||
|
||||
// console.log('some test',example.some.test)
|
||||
|
||||
example.some.thing=15
|
||||
console.log('--------------------------')
|
||||
console.log('instance after adding rx to properties\n')
|
||||
console.log(example)
|
||||
console.log('--------------------------')
|
||||
|
||||
console.log('now removing reactivity from \'test\'')
|
||||
example.rxRemove('test')
|
||||
example.setPath('test',42)
|
||||
console.log('\'test\' now holds unreactive value')
|
||||
console.log('now removing reactivity from \'test\' again')
|
||||
example.rxRemove('test')
|
||||
console.log('--------------------------')
|
||||
console.log('instance after removing rx from \'test\'\n')
|
||||
console.log(example)
|
||||
console.log('--------------------------')
|
||||
|
||||
// console.log('some test',example.some.test)
|
|
@ -0,0 +1,47 @@
|
|||
import RxClass from '../src/rx-class.js'
|
||||
|
||||
class Example extends RxClass {
|
||||
constructor(opts={}){
|
||||
super(opts)
|
||||
this.test = {
|
||||
yesterday: {bar:'bust', foo:3},
|
||||
today: {bing:5, bong:{who:'first',what:'second'}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const example = new Example({
|
||||
_rx_ : {
|
||||
handler: value => {console.log('-----------default subscription handler-------', value)},
|
||||
skip: 0
|
||||
}
|
||||
})
|
||||
|
||||
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))
|
||||
example.on('test', (val) => console.log('emitted value of test',val))
|
||||
|
||||
console.log('traversing \'test\' and making all leaves reactive')
|
||||
example.rxAdd('test',{traverse:true})
|
||||
|
||||
example.test.today.bong.what = 'third'
|
||||
example.setPath('test.today.bong.what','forth')
|
||||
example.setPath('test.today.bong.why','do not know')
|
||||
|
||||
console.log('\'test\', values object >', example.getPath('test'))
|
||||
console.log('\'test\', raw object >', example.getPath('test',true))
|
||||
console.log('--------------------------')
|
||||
console.log('instance after adding rx to leaves\n')
|
||||
console.dir(example.test)
|
||||
console.dir(example._rx_.props)
|
||||
console.log('--------------------------')
|
||||
console.log('now remove all rx on \'test.yesterday\'\n')
|
||||
example.rxRemove('test.today',{traverse:true})
|
||||
console.dir(example.test)
|
||||
console.log('rx props')
|
||||
console.dir(example._rx_.props)
|
||||
console.log('--------------------------')
|
||||
console.dir(example)
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignore":["examples/*.json"]
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "@uci-utils/data-store-rx",
|
||||
"version": "0.1.3",
|
||||
"description": "A Extended Class Data Store Module ",
|
||||
"main": "src/datastore.js",
|
||||
"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"
|
||||
},
|
||||
"author": "David Kebler",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/uCOMmandIt/.git"
|
||||
},
|
||||
"keywords": [
|
||||
"node.js"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/uCOMmandIt/uci-utils/issues"
|
||||
},
|
||||
"homepage": "https://github.com/uCOMmandIt/uci-utils#readme",
|
||||
"dependencies": {
|
||||
"data-store": "^4.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",
|
||||
"set-value": "^3.0.2",
|
||||
"traverse": "^0.6.6",
|
||||
"unset-value": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.2.0",
|
||||
"esm": "^3.2.25",
|
||||
"mocha": "^7.2.0",
|
||||
"nodemon": "^2.0.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
# uCOMmandIt Reactive Class
|
||||
|
||||
A Great Class to Extend has built in support for make reactive any/all class properties and their nested leaves.
|
||||
|
||||
## 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.
|
||||
|
||||
## Features
|
||||
|
||||
Will both emit and allow subscriptions to any property 'path' that has been made reactive.
|
||||
|
||||
Allows custom registration of unlimited state change event hooks.
|
||||
|
||||
Allows additional rxjs operators for the pipe the observer for each property.
|
||||
|
||||
See Examples folder
|
|
@ -0,0 +1,181 @@
|
|||
import { EventEmitter } from 'events'
|
||||
import { BehaviorSubject, from } from 'rxjs'
|
||||
import { distinctUntilChanged, skip } from 'rxjs/operators'
|
||||
// ** nested object access **
|
||||
import _get from 'get-value'
|
||||
import _set from 'set-value'
|
||||
import _del from 'unset-value'
|
||||
// *********************
|
||||
import equal from 'deep-equal'
|
||||
import traverse from 'traverse'
|
||||
import isPlainObject from 'is-plain-object'
|
||||
|
||||
export default class RxClass extends EventEmitter {
|
||||
constructor(opts){
|
||||
// console.log('constructor', opts)
|
||||
super(opts)
|
||||
this._rx_ = {
|
||||
event: opts._rx_.event || 'changed',
|
||||
handler: opts._rx_.handler,
|
||||
props: {},
|
||||
hooks: [],
|
||||
operators:[],
|
||||
skip: opts._rx_.skip == null ? 1 : opts._rx_.skip
|
||||
}
|
||||
}
|
||||
|
||||
// if property exists it moves value to _ version and creates getter and setter and behavior subject
|
||||
rxAdd(opath,opts={}) {
|
||||
const path = this._rxFormatPath(opath)
|
||||
if (opts.traverse) {
|
||||
const self = this
|
||||
traverse(_get(this,path)).map(function () {
|
||||
if (this.isLeaf) {
|
||||
const lpath = this.path
|
||||
lpath.unshift(path)
|
||||
// console.log('leaf',lpath,this.node)
|
||||
self.rxAdd(lpath)
|
||||
// console.log(this.get())
|
||||
}
|
||||
}
|
||||
)
|
||||
return
|
||||
} // end traverse
|
||||
const value = _get(this,path)
|
||||
if (value === undefined) {
|
||||
console.log ('no current property for', path)
|
||||
_set(this,path,null)
|
||||
}
|
||||
const {parent,name} = this.rxGetObj(path,'__parent__')
|
||||
const rx = this.setPath(['_rx_.props',path],{})
|
||||
rx.value = value
|
||||
rx.obs = from(new BehaviorSubject({value:value, path:path}).pipe(
|
||||
// rx.obs = from(new ReplaySubject(1).pipe(
|
||||
skip(this._rx_.skip),
|
||||
distinctUntilChanged()
|
||||
// ,
|
||||
// takeUntil(_get(this._deleted,path))
|
||||
))
|
||||
rx.subs = {}
|
||||
this.rxSubscribe(rx,this._rx_.handler,'_default')
|
||||
this.rxSubscribe(rx,(opts.subscribe ||{}).handler,(opts.subscribe || {}).name)
|
||||
|
||||
const self = this
|
||||
Object.defineProperty(parent, name, {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get () {return rx.value},
|
||||
set (value) {
|
||||
rx.value = value
|
||||
rx.obs.next({value:value, path:path})
|
||||
self.emit(opts.event || self._rx_.event,{value:value, path:path})
|
||||
self.emit(path,value)
|
||||
self._rx_.hooks.forEach(hook=>hook.call(self,{value:value, path:path}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// if name is only passed remove
|
||||
rxHook(func,name) {
|
||||
this._rx_.hooks[name]=func
|
||||
}
|
||||
|
||||
// if name is only passed remove
|
||||
rxOperator(func,name) {
|
||||
this._rx_.operators[name]=func
|
||||
}
|
||||
|
||||
// pass '__new__' when getting props for new rx object
|
||||
rxGetObj(path,prop) {
|
||||
path = this._rxFormatPath(path)
|
||||
path = path.split('.')
|
||||
const name = path.pop()
|
||||
path = path.join('.')
|
||||
let parent = path ? _get(this,path) : this
|
||||
// console.log(name,path,parent)
|
||||
if (parent === null) parent = {}
|
||||
if (prop==='__parent__') return {name:name, parent:parent, parentPath:path}
|
||||
const rx = this.getPath(['_rx_.props',path,name],true)
|
||||
// console.log(path,name,'reactive object',rx)
|
||||
return prop ? (rx || {})[prop] : rx
|
||||
}
|
||||
|
||||
// removes obser, subscriptions, and getter and setter and make back into plain value
|
||||
rxRemove(path,opts={}) {
|
||||
if (opts.traverse) {
|
||||
const self = this
|
||||
traverse(_get(this,path)).map(function () {
|
||||
if (this.isLeaf) {
|
||||
const lpath = this.path
|
||||
lpath.unshift(path)
|
||||
self.rxRemove(lpath)
|
||||
}
|
||||
}
|
||||
)
|
||||
this.delPath(['_rx_.props',path],true)
|
||||
return
|
||||
}
|
||||
let { parent, name, parentPath } = this.rxGetObj(path,'__parent__')
|
||||
const rxparent = this.getPath(['_rx_.props',parentPath],true)
|
||||
const rx = rxparent[name]
|
||||
if (rx && Object.getOwnPropertyDescriptor(parent, name)['get']) {
|
||||
Object.values(rx.subs).forEach(sub=>sub.unsubscribe())
|
||||
delete parent[name]
|
||||
parent[name] = rx.value
|
||||
delete rxparent[name]
|
||||
} // else console.warn(`property at '${path}' was not reative, remove aborted`)
|
||||
}
|
||||
|
||||
// pass rx as object or path
|
||||
rxSubscribe(rxObj,handler,name){
|
||||
// console.log('subscription',rxObj)
|
||||
if (typeof name !== 'string') return
|
||||
rxObj = (typeof rxObj === 'string') ? this.rxGetObj(rxObj) : rxObj
|
||||
if (typeof handler==='function') rxObj.subs[name] = rxObj.obs.subscribe(handler)
|
||||
}
|
||||
|
||||
_rxFormatPath(path) {
|
||||
if (path==null) return path
|
||||
if (Array.isArray(path)) {
|
||||
path = path.filter(Boolean).join('.')
|
||||
} else path = path.replace(/\/:/g, '.')
|
||||
return path
|
||||
}
|
||||
|
||||
setObjPath () {return _set(...arguments)}
|
||||
getObjPath () {return _get(...arguments)}
|
||||
delObjPath () { return _del(...arguments)}
|
||||
|
||||
delPath(path,confirm) {
|
||||
path = this._rxFormatPath(path)
|
||||
if (confirm) _del(this,path)
|
||||
else console.warn('must confirm to delete path',path)
|
||||
}
|
||||
|
||||
getPath(path,raw){
|
||||
path = this._rxFormatPath(path)
|
||||
const value = _get(this,path)
|
||||
if (!isPlainObject(value) || raw) return value
|
||||
let obj = {}
|
||||
traverse(_get(this,path)).map(function () {
|
||||
if (this.isLeaf) {
|
||||
// let leaf = this.path // this path is immutable
|
||||
// console.log(this.path,this.node)
|
||||
// leaf = leaf.pop()
|
||||
_set(obj,this.path.join('.'),this.node)
|
||||
}
|
||||
}
|
||||
)
|
||||
return obj
|
||||
}
|
||||
|
||||
setPath(path,value){
|
||||
path = this._rxFormatPath(path)
|
||||
const curValue = _get(this,path)
|
||||
if (!equal(curValue,value) || !value) {
|
||||
_set(this,path,value)
|
||||
}
|
||||
return _get(this,path)
|
||||
}
|
||||
|
||||
} // end class
|
Loading…
Reference in New Issue