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 edata
Browser
<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: firstName
use 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: Hi
get 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 Earth
to 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
edata
function 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
edataFactory
is 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,
root
is 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
change
edata object to it that filtered from(from||root).change
edata object, the default filter is to test if theroot.path
starts 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
,context
find 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
get
method. Thedescriptor
only 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 setvalue
based on previous value, thedescriptor
only applied whenempty
istrue
.
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 onlyset
when the path not exists orinvalid
test true for the path, otherwise perform aget
operation.
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_edata
orvalue
inpath
*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
wrapper
on 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 fromdescriptors
with 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 withmapFunc
as 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
value
into 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}