rdata v0.1.5
edata
Turn javascript data into extended EventEmitter.
An edata is an EventEmitter with .value (getter/setter), and helper methods to manipulate it.
Install
NPM
npm i -S edataBrowser
<script src="https://unpkg.com/edata"></script>
<script>
// edata is a global
edata...
</script>Usage
- Initialize edata
import edata from 'edata'
const edataFactory = edata({})
// build edata model object
const model = edataFactory({
age: 20,
firstName: 'Hello',
lastName: 'World',
address: {
city: 'Earth'
}
})model and everything inside is an edata (an EventEmitter with .value), so
edata = EventEmitter + '.value'
the edata.value is a getter/setter
model.value.firstName.value // get: firstName
model.value.firstName.value = '' // set: firstNameuse edata.on to listen on value changes
model.value.firstName.on('data', newVal=>{
console.log('First Name changed to: ' + newVal)
})
model.value.firstName.value = 'Hi'
//[console] First Name changed to: Higet an edata from path
const city = model.get('address.city')
//instead of:
// const city = model.value.address.value.city
city.value = 'Earth'every edata is an EventEmitter, so
model.get('address.city').on('data', newVal=>console.log('new value:', newVal))
model.set('address', {city: 'Mars'}) // set to address.city, same as above!
model.unwrap('address') // flatten: {city: 'Earth'}
model.unset('address') // delete model.address
model.unwrap() // flatten all: {age: 20, firstName: 'Hello', lastName: 'World'}Notice all edata object has default valueOf function that returns value, so below are same:
model.get('age').value + 10 // 30
// same as:
model.get('age') + 10 // 30- Observe model changes
The root model has a change attribute, which is also an edata, you can callback for every changes.
observe changes of model
const onDataChange = ({data, type, path})=>{
console.log('data mutated:', path, type, data.unwrap())
}
model.change.on('data', onDataChange)model.set('address.city', 'Mars')
// [console] data mutated: [ 'address', 'city' ] add Mars
model.get('address.city').value = 'Earth'
// [console] data mutated: [ 'address', 'city' ] change Earth
model.unset('address.city')
// [console] data mutated: [ 'address', 'city' ] delete Earthto stop, you can .off the event any time!
model.change.off('data', onDataChange)- Define Data Relations
You can define data relations using setComputed, as below:
const firstName = model.get('firstName')
const lastName = model.get('lastName')
// set fullName = firstName + ' ' + lastName
model.setComputed(
'fullName',
['firstName', 'lastName'],
(a, b) => a + ' ' + b
)
model.get('fullName').on('data', val => console.log(val))
firstName.value = 'Green'
// [console] Green World
model.unwrap()
// {firstName:'Green', lastName:'World', fullName:'Green World'}- Use in React
const model = edata()({user: {name: 'earth'}})
class App extends React.Component {
constructor(props){
super(props)
const {model} = this.props
this.state = model.unwrap()
// init: {name: 'earth'}
this.onInputChange = e => {
const {name, value} = e.target
model.set(name, value)
}
this.onModelChange = ({data, type, path})=>{
this.setState({
[path]: data.value
})
}
}
componentDidMount(){
model.change.on('data', this.onModelChange)
}
componentWillUnmount(){
model.change.off('data', this.onModelChange)
}
render(){
const {model} = this.props
const userName = model.unwrap('name')
return <div>
<h3>Hello {userName}</h3>
<input name='name' value={userName} onChange={this.onInputChange} />
</div>
}
}
ReactDOM.render(<App model={model.slice('user')} />, app)You can play with the demo here
API
- import edata, {DefaultClass} from 'edata'
The lib expose a default
edatafunction to use
The DefaultClass can be used for sub-class your own implemention of edata.
You can extends this class to add your own methods:
class MyedataClass extends DefaultClass {
added_method(){
// do sth.
}
}Notice
Be careful when using above class keyword, by default, you have to transpile your code to ES5 to run correctly.
If you need to use class without transpile, you should import edata/dist/node, or edata/dist/es, the different between the two is the node version use native events package for EventEmitter.
- edataFactory = edata(options)
the
edataFactoryis used to turn data into wrapped_edata.
A wrapped_edata is an edata with some helper methods, like get, set etc., so
wrapped_edata = EventEmitter + '.value' + '.get' + '.set' ...options has below options:
- WrapClass: Default implementation is here
- unwrapConfig: when
unwrap, you can add default config - addMethods: You can add your own API with this option
return: function(data) -> wrapped_edata
import edata, {DefaultClass} from 'edata'
class MyedataClass extends DefaultClass {
map(fn) {
this.on('data', fn)
return () => this.off('data', fn)
}
}
var edataFactory = edata({
WrapClass: MyedataClass
})
const root1 = edataFactory(data1)
const root2 = edataFactory(data2)
root1.map(onChangeHandler)
...- root = edataFactory(data: any)
the above code example,
rootis a wrapped_edata, with all nested data wrapped.
return: wrapped_edata for data
root.change is also an edata object, you can listen to data event for children changes.
Any data inside root is a wrapped_edata, and may be contained by {} or [] edata object, keep the same structure as before.
Any wrapped_edata have root and path propperties, get, set, ... helper functions.
var root = edataFactory({x: {y: {z: 1}}})
root.some_api...- wrapped_edata.get(path: string|string[])
get nested wrapped data from path, path is array of string or dot(
".") seperated string.
return: wrapped_edata at path
var z = root.get('x.y.z')
// or
var z = root.get(['x','y','z'])
z.value // 2
z.value = 10- wrapped_edata.slice(path: string|string[], filter?: ({data, type, path}):boolean, from = root)
get nested wrapped data from path, and attach a
changeedata object to it that filtered from(from||root).changeedata object, the default filter is to test if theroot.pathstarts with path.
return: wrapped_edata, which have a .change edata object
The wrapped_edata.change edata object's value has path property to reflect the sub path of the sliced data.
var xy = root.slice('x.y')
xy.change.on('data', ({data, type, path})=>console.log(type, path))
xy.set('z', 1)
// x.y changed! ['z']- wrapped_edata.context(path: string|string[])
Roughly the opposite to
slice,contextfind model from closest parent, with matching path usingRegExp.
Passing "" will return root model.
return: wrapped_edata or undefined if not find
var xy = root.get('x.y')
var x = xy.context('x') // get closest x- wrapped_edata.set(path?: string|string[], value?: any, descriptor?: object)
set nested wrapped data value from path, same rule as
getmethod. Thedescriptoronly applied when path not exists.
return: wrapped_edata for value, at path
path can contain a.[3] alike string denote 3 is an array element of a.
value can be any data types, if path is omitted, set value into wrapped_edata itself.
If value is a edata object, then it's an atom data, which will not be wrapped inside.
descriptor is optional, same as 3rd argument of Object.defineProperty, this can e.g. create non-enumerable edata object which will be hidden when unwrap.
If data not exist in path, all intermediate object will be created.
var z = root.set('x.a', 10)
z.value // 10
// same as: (only if x.a exits)
root.get('x.a').set(10)
root.get('x.a').value = 10
var z = root.set('x.c', [], {enumerable: false}) // c is non-enumerable
Object.keys( z.get('x').value ) // ['a']
root.unwrap() // {x: {y: {z: 1}}, a: 10} // `c` is hidden!
root.set(`arr.[0]`, 10)
root.get('arr.0').value // 10
root.unwrap() // {x: {y: {z: 1}}, a: 10, arr:[10]} // `arr` is array!- wrapped_edata.getset(path?: string|string[], function(prevValue:wrappedData|any, empty?: boolean)->newValue, descriptor: object)
like
set, but value is from a function, it let you setvaluebased on previous value, thedescriptoronly applied whenemptyistrue.
return: wrapped_edata for newValue, at path
// x.a = 10
var z = root.getset('x.a', val=>val + 1)
z.value // 11- wrapped_edata.ensure(invalid?: (val:wrapped):boolean, path: string|string[], value?: any, descriptor?: object)
like
set, but onlysetwhen the path not exists orinvalidtest true for the path, otherwise perform agetoperation.
The invalid test more like a set then get when specified.
return: wrapped_edata at path
var z = root.ensure('x.a', 5)
// x.a exists, so perform a get, `5` ignored
z.value // 11
var z = root.ensure('x.b', 5)
// x.b not exists, so perform a `set`
z.value // 5
// ensure `a.b` always >= 10
root.ensure(val=>val<10, 'x.b', 10).unwrap() //10- wrapped_edata.unset(path: string|string[])
delete
wrapped_edataorvalueinpath
*return: deleted data been unwrapped*
var z = root.unset('x.b')
z // 5- wrapped_edata.unwrap(path?: string|string[], config?: {json: true})
unwrap data and nested data while keep data structure, any level of
wrapperon any data will be stripped.
If set config arg with {json: true}, then any circular referenced data will be set undefined, suitable for JSON.stringify.
If set config arg with {map: value=>...}, then the final value is first mapped, then returned, and the return value of unwrapConfig will be merged into this config.
return: unwrapped data
var z = root.unwrap()
z // {x: {y: {z: 11}}, a: [10]}, x.c is hidden- wrapped_edata.setMany(kvMap: object, descriptors?: object)
multiple set key and value from
kvMap, and find descriptor fromdescriptorswith the key.
return: object with same key, and each value is result of set()
root.unwrap() // {a:10, x:20, y:30}
root.setMany({
x:1,
y:2
})
root.unwrap() // {a:10, x: 1, y:2}- wrapped_edata.getMany(pathMap: object|string[]|string, mapFunc?:(val: IWrappedData|undefined)=>any)
multiple get each path from
pathMap(can be array/object/string), and map each value withmapFuncas result.
return: result data with same shape as pathMap
root.unwrap() // {a:10, x:20, y:30}
root.getMany(['x', 'y']) // [20, 30]- wrapped_array.push(value: any)
push new
valueinto wrapped data when it's array, all the inside will be wrapped.
return: newly pushed wrapped_edata
var z = root.set('d', [])
z.push({v: 10})
z.get('d.0.v').value // 10- wrapped_array.pop()
pop and unwrap last element in wrapped array.
return: unwrapped data in last array element
var z = root.ensure('d', [])
z.get('d').pop() // {v: 10}