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...
</script>
Usage
- Initialize edata
import edata from 'edata'
const edataFactory = edata({})
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
model.value.firstName.value = ''
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'
get an edata
from path
const city = model.get('address.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'})
model.unwrap('address')
model.unset('address')
model.unwrap()
Notice all edata object
has default valueOf
function that returns value
, so below are same:
model.get('age').value + 10
model.get('age') + 10
- 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')
model.get('address.city').value = 'Earth'
model.unset('address.city')
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')
model.setComputed(
'fullName',
['firstName', 'lastName'],
(a, b) => a + ' ' + b
)
model.get('fullName').on('data', val => console.log(val))
firstName.value = 'Green'
model.unwrap()
- 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()
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(){
}
}
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')
var z = root.get(['x','y','z'])
z.value
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 the root.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)
- wrapped_edata.context(path: string|string[])
Roughly the opposite to slice
, context
find model from closest parent, with matching path using RegExp
.
Passing ""
will return root model
.
return: wrapped_edata
or undefined
if not find
var xy = root.get('x.y')
var x = xy.context('x')
- wrapped_edata.set(path?: string|string[], value?: any, descriptor?: object)
set nested wrapped data value from path, same rule as get
method. The descriptor
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
root.get('x.a').set(10)
root.get('x.a').value = 10
var z = root.set('x.c', [], {enumerable: false})
Object.keys( z.get('x').value )
root.unwrap()
root.set(`arr.[0]`, 10)
root.get('arr.0').value
root.unwrap()
- 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 set value
based on previous value, the descriptor
only applied when empty
is true
.
return: wrapped_edata for newValue
, at path
var z = root.getset('x.a', val=>val + 1)
z.value
- wrapped_edata.ensure(invalid?: (val:wrapped):boolean, path: string|string[], value?: any, descriptor?: object)
like set
, but only set
when the path not exists or invalid
test true for the path, otherwise perform a get
operation.
The invalid
test more like a set then get when specified.
return: wrapped_edata at path
var z = root.ensure('x.a', 5)
z.value
var z = root.ensure('x.b', 5)
z.value
root.ensure(val=>val<10, 'x.b', 10).unwrap()
- wrapped_edata.unset(path: string|string[])
delete wrapped_edata
or value
in path
return: deleted data been unwrapped
var z = root.unset('x.b')
z
- 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
- wrapped_edata.setMany(kvMap: object, descriptors?: object)
multiple set key and value from kvMap
, and find descriptor from descriptors
with the key.
return: object with same key, and each value is result of set()
root.unwrap()
root.setMany({
x:1,
y:2
})
root.unwrap()
- 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 with mapFunc
as result.
return: result data with same shape as pathMap
root.unwrap()
root.getMany(['x', 'y'])
- 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
- 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()