mutant
Version:
Create observables and map them to DOM elements. Massively inspired by hyperscript and observ-*. No virtual dom, just direct observable bindings. Unnecessary garbage collection is avoided by using mutable objects instead of blasting immutable junk all ove
177 lines (147 loc) • 4.09 kB
JavaScript
var Value = require('./value')
var LazyWatcher = require('./lib/lazy-watcher')
var isSame = require('./lib/is-same')
var resolve = require('./resolve')
var isObservable = require('./is-observable')
var forEachPair = require('./for-each-pair')
var addLookupMethods = require('./lib/add-lookup-methods')
module.exports = Dict
function Dict (defaultValues, opts) {
var object = Object.create({})
var sources = {}
var objectReleases = {}
var fixedIndexing = opts && opts.fixedIndexing || false
var releases = []
var comparer = opts && opts.comparer || null
var binder = LazyWatcher(update, listen, unlisten)
binder.value = object
if (opts && opts.nextTick) binder.nextTick = true
if (opts && opts.idle) binder.idle = true
if (defaultValues) {
forEachPair(defaultValues, put)
}
var observable = function MutantDictionary (listener) {
if (!listener) {
return binder.getValue()
}
return binder.addListener(listener)
}
addLookupMethods(observable, sources)
observable.put = function (key, valueOrObs) {
valueOrObs = getObsValue(valueOrObs)
put(key, valueOrObs)
binder.broadcast()
return valueOrObs
}
observable.clear = function () {
Object.keys(sources).forEach(function (key) {
tryInvoke(objectReleases[key])
delete sources[key]
delete objectReleases[key]
delete object[key]
})
binder.broadcast()
}
observable.delete = function (key) {
tryInvoke(objectReleases[key])
delete sources[key]
delete objectReleases[key]
delete object[key]
binder.broadcast()
}
observable.transaction = function (cb) {
binder.transaction(observable, cb)
}
observable.set = function (values) {
if (fixedIndexing) {
var keys = []
forEachPair(values, function (key, value) {
keys.push(key)
if (sources[key]) {
sources[key].set(value)
} else {
put(key, getObsValue(value))
}
})
Object.keys(sources).forEach(function (key) {
if (!keys.includes(key)) {
tryInvoke(objectReleases[key])
delete sources[key]
delete objectReleases[key]
delete object[key]
}
})
} else {
Object.keys(sources).forEach(function (key) {
tryInvoke(objectReleases[key])
delete sources[key]
delete objectReleases[key]
delete object[key]
})
forEachPair(values, put)
binder.broadcast()
}
}
return observable
// scoped
function getObsValue (valueOrObs) {
if (fixedIndexing && !isObservable(valueOrObs)) {
valueOrObs = Value(valueOrObs)
}
return valueOrObs
}
function put (key, valueOrObs) {
tryInvoke(objectReleases[key])
sources[key] = valueOrObs
if (binder.live) {
objectReleases[key] = bind(key, valueOrObs)
}
object[key] = resolve(valueOrObs)
}
function bind (key, valueOrObs) {
return typeof valueOrObs === 'function' ? valueOrObs(updateKey.bind(this, key)) : null
}
function updateKey (key, value) {
object[key] = value
binder.broadcast()
}
function listen () {
Object.keys(sources).forEach(function (key) {
objectReleases[key] = bind(key, sources[key])
})
if (opts && opts.onListen) {
var release = opts.onListen()
if (typeof release === 'function') {
releases.push(release)
}
}
}
function unlisten () {
Object.keys(sources).forEach(function (key) {
tryInvoke(objectReleases[key])
delete objectReleases[key]
})
while (releases.length) {
tryInvoke(releases.pop())
}
if (opts && opts.onUnlisten) {
opts.onUnlisten()
}
}
function update () {
var changed = false
Object.keys(sources).forEach(function (key) {
var newValue = resolve(sources[key])
if (!isSame(newValue, object[key], comparer)) {
object[key] = newValue
changed = true
}
})
return changed
}
}
function tryInvoke (func) {
if (typeof func === 'function') {
func()
}
}