Last active
March 10, 2018 04:25
-
-
Save nestharus/13b4d74f2ef4a2f4357dbd3fc23c1e54 to your computer and use it in GitHub Desktop.
mobx observable map
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function once(func) { | |
let invoked = false; | |
return function() { | |
if (invoked) { | |
return; | |
} | |
invoked = true; | |
return func(); | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { isObservable, observable } from 'mobx'; | |
import { ObservableMap, isPlainObject, isModifierDescriptor } from './index'; | |
export function referenceEnhancer(value) { | |
return value; | |
} | |
export function deepEnhancer(v, _, name) { | |
if (isModifierDescriptor(v)) { | |
throw new Error( | |
"You tried to assign a modifier wrapped value to a collection, please define modifiers when creating the collection, not when modifying it" | |
); | |
} | |
if (isObservable(v)) { | |
return v; | |
} | |
if (Array.isArray(v)) { | |
return observable.array(v, name); | |
} | |
if (isPlainObject(v)) { | |
return observable.object(v, name); | |
} | |
if (v instanceof Map) { | |
return new ObservableMap(v, name); | |
} | |
return v; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export * from './disposer'; | |
export * from './enhance'; | |
export * from './intercept'; | |
export * from './modifier'; | |
export * from './object'; | |
export * from './types-index'; | |
export * from './observe'; | |
export * from './reaction'; | |
export * from './state'; | |
export * from './to-js'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
isBoxedObservable, | |
isObservable, | |
intercept, | |
observe | |
} from 'mobx'; | |
import { once } from './index'; | |
export function hasInterceptors(administration) { | |
return administration.interceptors && administration.interceptors.length > 0; | |
} | |
export function interceptChange(administration, change) { | |
if (!administration.interceptors) { return; } | |
for (let interceptor of administration.interceptors) { | |
change = interceptor(change); | |
if (change && typeof change !== 'object') { | |
throw new Error('Intercept handlers should return nothing or a change object'); | |
} | |
if (!change) { | |
break; | |
} | |
} | |
return change; | |
} | |
export function registerInterceptor(administration, interceptor) { | |
const interceptors = administration.interceptors || (administration.interceptors = []); | |
interceptors.push(interceptor); | |
return once(() => { | |
const idx = interceptors.indexOf(interceptor); | |
if (idx !== -1) { | |
interceptors.splice(idx, 1); | |
} | |
}); | |
} | |
export function interceptDeep(target, propertyName, interceptor) { | |
const hasProperty = typeof propertyName === 'string'; | |
const isBoxed = isBoxedObservable(target); | |
if (hasProperty || isBoxed) { | |
let disposer; | |
let disposer2; | |
let disposer3; | |
let remakeObserver; | |
if (hasProperty) { | |
remakeObserver = function() { | |
if (disposer2 !== undefined) { | |
disposer2(); | |
disposer2 = undefined; | |
} | |
if (isObservable(target[propertyName])) { | |
disposer2 = intercept(target[propertyName], interceptor); | |
} | |
}; | |
disposer = intercept(target, propertyName, function(change) { | |
const newChange = interceptor({ | |
type: 'root', | |
object: target, | |
oldValue: target[propertyName], | |
newValue: change.newValue | |
}); | |
if (newChange) { | |
change.newValue = newChange.newValue; | |
} | |
else { | |
change = undefined; | |
} | |
return change; | |
}); | |
} | |
else { | |
remakeObserver = function() { | |
if (disposer2 !== undefined) { | |
disposer2(); | |
disposer2 = undefined; | |
} | |
if (isObservable(target.get())) { | |
disposer2 = intercept(target.get(), interceptor); | |
} | |
}; | |
disposer = intercept(target, function(change) { | |
const newChange = interceptor({ | |
type: 'root', | |
object: target, | |
oldValue: target.get(), | |
newValue: change.newValue | |
}); | |
if (newChange) { | |
change.newValue = newChange.newValue; | |
} | |
else { | |
change = undefined; | |
} | |
return change; | |
}); | |
} | |
disposer3 = observe(target, propertyName, function() { | |
remakeObserver(); | |
}, true); | |
return function() { | |
if (disposer !== undefined) { | |
disposer(); | |
disposer = undefined; | |
} | |
if (disposer2 !== undefined) { | |
disposer2(); | |
disposer2 = undefined; | |
} | |
if (disposer3 !== undefined) { | |
disposer3(); | |
disposer3 = undefined; | |
} | |
}; | |
} | |
else { | |
return intercept(target, propertyName, interceptor); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { interceptDeep } from './index'; | |
import { observable } from 'mobx'; | |
function setTargetValue(target, propertyName, value) { | |
if (propertyName === undefined) { | |
target.set(value); | |
} | |
else { | |
target[propertyName] = value; | |
} | |
} | |
function getTargetValue(target, propertyName) { | |
if (propertyName === undefined) { | |
return target.get(); | |
} | |
return target[propertyName]; | |
} | |
describe('#Intercept Deep', () => { | |
it('intercept', (done) => { | |
function performTest(target, propertyName) { | |
let ranRoot = 0; | |
let ranIntercept = 0; | |
let obs = interceptDeep(target, propertyName, (change) => { | |
if (change.type === 'root') { | |
++ranRoot; | |
return change; | |
} | |
else { | |
++ranIntercept; | |
return null; | |
} | |
}); | |
let value = getTargetValue(target, propertyName); | |
if (value !== null && typeof value === 'object' && 'a' in value) { | |
const old = value.a; | |
value.a = 8; | |
--ranIntercept; | |
expect(value.a).toEqual(old); | |
} | |
setTargetValue(target, propertyName, observable([])); | |
expect(getTargetValue(target, propertyName)).toEqual(observable([])); | |
getTargetValue(target, propertyName).push(5); | |
expect(getTargetValue(target, propertyName)).toEqual(observable([])); | |
setTargetValue(target, propertyName, observable(6)); | |
expect(getTargetValue(target, propertyName).get()).toEqual(6); | |
getTargetValue(target, propertyName).set(7); | |
expect(getTargetValue(target, propertyName).get()).toEqual(6); | |
setTargetValue(target, propertyName, []); | |
expect(getTargetValue(target, propertyName)).toEqual([]); | |
getTargetValue(target, propertyName).push(14); | |
expect(getTargetValue(target, propertyName)).toEqual([14]); | |
obs(); | |
setTargetValue(target, propertyName, observable([])); | |
expect(getTargetValue(target, propertyName)).toEqual(observable([])); | |
getTargetValue(target, propertyName).push(7); | |
expect(getTargetValue(target, propertyName)).toEqual(observable([7])); | |
expect(ranRoot).toBe(3); | |
expect(ranIntercept).toBe(2); | |
} | |
performTest(observable.shallowBox(null)); | |
performTest(observable.shallowObject({ a: null }), 'a'); | |
performTest(observable.shallowObject({ a: observable.shallowObject({ a: 55 }) }), 'a'); | |
done(); | |
}); | |
it('intercept reverse', (done) => { | |
function performTest(target, propertyName) { | |
const original = getTargetValue(target, propertyName); | |
let ranRoot = 0; | |
let ranIntercept = 0; | |
let obs = interceptDeep(target, propertyName, (change) => { | |
if (change.type === 'root') { | |
++ranRoot; | |
return null; | |
} | |
else { | |
++ranIntercept; | |
return change; | |
} | |
}); | |
let value = getTargetValue(target, propertyName); | |
if (value !== null && typeof value === 'object' && 'a' in value) { | |
value.a = 8; | |
--ranIntercept; | |
expect(value.a).toEqual(8); | |
} | |
setTargetValue(target, propertyName, observable([])); | |
expect(getTargetValue(target, propertyName)).toEqual(original); | |
setTargetValue(target, propertyName, observable(6)); | |
expect(getTargetValue(target, propertyName)).toEqual(original); | |
setTargetValue(target, propertyName, []); | |
expect(getTargetValue(target, propertyName)).toEqual(original); | |
obs(); | |
setTargetValue(target, propertyName, observable([])); | |
expect(getTargetValue(target, propertyName)).toEqual(observable([])); | |
getTargetValue(target, propertyName).push(7); | |
expect(getTargetValue(target, propertyName)).toEqual(observable([7])); | |
expect(ranRoot).toBe(3); | |
expect(ranIntercept).toBe(0); | |
} | |
performTest(observable.shallowBox(null)); | |
performTest(observable.shallowObject({ a: null }), 'a'); | |
performTest(observable.shallowObject({ a: observable.shallowObject({ a: 55 }) }), 'a'); | |
done(); | |
}); | |
it('intercept normal', (done) => { | |
function performTest(target, propertyName) { | |
let ranRoot = 0; | |
let ranIntercept = 0; | |
let obs = interceptDeep(target, (change) => { | |
if (change.type === 'root') { | |
++ranRoot; | |
return null; | |
} | |
else { | |
++ranIntercept; | |
return change; | |
} | |
}); | |
let value = getTargetValue(target, propertyName); | |
if (value !== null && typeof value === 'object' && 'a' in value) { | |
value.a = 8; | |
expect(value.a).toEqual(8); | |
} | |
setTargetValue(target, propertyName, observable([])); | |
expect(getTargetValue(target, propertyName)).toEqual(observable([])); | |
getTargetValue(target, propertyName).push(5); | |
expect(getTargetValue(target, propertyName)).toEqual(observable([5])); | |
setTargetValue(target, propertyName, observable(6)); | |
expect(getTargetValue(target, propertyName).get()).toEqual(6); | |
getTargetValue(target, propertyName).set(7); | |
expect(getTargetValue(target, propertyName).get()).toEqual(7); | |
setTargetValue(target, propertyName, []); | |
expect(getTargetValue(target, propertyName)).toEqual([]); | |
getTargetValue(target, propertyName).push(14); | |
expect(getTargetValue(target, propertyName)).toEqual([14]); | |
obs(); | |
setTargetValue(target, propertyName, observable([])); | |
expect(getTargetValue(target, propertyName)).toEqual(observable([])); | |
getTargetValue(target, propertyName).push(7); | |
expect(getTargetValue(target, propertyName)).toEqual(observable([7])); | |
expect(ranRoot).toBe(0); | |
expect(ranIntercept).toBe(3); | |
} | |
performTest(observable.shallowObject({ a: null }), 'a'); | |
performTest(observable.shallowObject({ a: observable.shallowObject({ a: 55 }) }), 'a'); | |
done(); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function isModifierDescriptor(thing) { | |
return thing !== null && typeof thing === 'object' && thing.isMobxModifierDescriptor === true; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function isPlainObject(value) { | |
if (value === undefined || typeof value !== 'object') { | |
return false; | |
} | |
const proto = Object.getPrototypeOf(value); | |
return proto === Object.prototype || proto === null; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
isBoxedObservable, | |
isObservable, | |
observe, | |
} from 'mobx'; | |
import { once } from './index'; | |
export function hasListeners(administration) { | |
return administration.changeListeners && administration.changeListeners.length > 0; | |
} | |
export function notifyListeners(administration, change) { | |
const listeners = administration.changeListeners; | |
if (!listeners) { | |
return; | |
} | |
for (let listener of listeners) { | |
listener(change); | |
} | |
} | |
export function registerListener(administration, listener) { | |
const listeners = administration.changeListeners || (administration.changeListeners = []); | |
listeners.push(listener); | |
return once(() => { | |
const index = listeners.indexOf(listener); | |
if (index !== -1) { | |
listeners.splice(index, 1); | |
} | |
}); | |
} | |
export function observeDeep(target, propertyName, listener, invokeImmediately) { | |
const hasProperty = typeof propertyName === 'string'; | |
const isBoxed = isBoxedObservable(target); | |
if (hasProperty || isBoxed) { | |
let disposer; | |
let disposer2; | |
let remakeObserver; | |
if (hasProperty) { | |
remakeObserver = function(invokeImmediately) { | |
if (disposer2 !== undefined) { | |
disposer2(); | |
disposer2 = undefined; | |
} | |
if (isObservable(target[propertyName])) { | |
disposer2 = observe(target[propertyName], listener, invokeImmediately); | |
} | |
}; | |
} | |
else { | |
remakeObserver = function(invokeImmediately) { | |
if (disposer2 !== undefined) { | |
disposer2(); | |
disposer2 = undefined; | |
} | |
if (isObservable(target.get())) { | |
disposer2 = observe(target.get(), listener, invokeImmediately); | |
} | |
}; | |
} | |
disposer = observe(target, propertyName, function(change) { | |
remakeObserver(); | |
listener({ | |
type: 'root', | |
object: target, | |
oldValue: change.oldValue, | |
newValue: change.newValue | |
}); | |
}); | |
remakeObserver(invokeImmediately); | |
return function() { | |
if (disposer !== undefined) { | |
disposer(); | |
disposer = undefined; | |
} | |
if (disposer2 !== undefined) { | |
disposer2(); | |
disposer2 = undefined; | |
} | |
}; | |
} | |
else { | |
return observe(target, propertyName, listener, invokeImmediately); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { observeDeep } from './index'; | |
import { observable, isObservable } from 'mobx'; | |
function setTargetValue(target, propertyName, value) { | |
if (propertyName === undefined) { | |
target.set(value); | |
} | |
else { | |
target[propertyName] = value; | |
} | |
} | |
function getTargetValue(target, propertyName) { | |
if (propertyName === undefined) { | |
return target.get(); | |
} | |
return target[propertyName]; | |
} | |
describe('#Observe', () => { | |
it('observe', (done) => { | |
function performTest(target, propertyName) { | |
let ranRoot = 0; | |
let ranSplice = 0; | |
let changeUpdate = 0; | |
let obs = observeDeep(target, propertyName, (change) => { | |
if (change.type === 'root') { | |
++ranRoot; | |
} | |
else if (change.type === 'splice') { | |
++ranSplice; | |
} | |
else if (change.type === 'update') { | |
++changeUpdate; | |
} | |
}); | |
let value = getTargetValue(target, propertyName); | |
if (value !== null && typeof value === 'object' && 'a' in value) { | |
value.a = 8; | |
--changeUpdate; | |
} | |
setTargetValue(target, propertyName, observable([])); | |
getTargetValue(target, propertyName).push(5); | |
setTargetValue(target, propertyName, observable(6)); | |
getTargetValue(target, propertyName).set(7); | |
expect(getTargetValue(target, propertyName).get()).toBe(7); | |
setTargetValue(target, propertyName, []); | |
getTargetValue(target, propertyName).push(14); | |
obs(); | |
setTargetValue(target, propertyName, observable([])); | |
getTargetValue(target, propertyName).push(7); | |
expect(ranRoot).toBe(3); | |
expect(ranSplice).toBe(1); | |
expect(changeUpdate).toBe(1); | |
} | |
performTest(observable.shallowBox(null)); | |
performTest(observable.shallowObject({ a: null }), 'a'); | |
performTest(observable.shallowObject({ a: observable.shallowObject({ a: null }) }), 'a'); | |
done(); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { extras } from 'mobx'; | |
export function inReaction() { | |
return extras.getGlobalState().isRunningReactions; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { extras } from 'mobx'; | |
export function checkIfStateModificationsAreAllowed(administration) { | |
const hasObservers = administration.observers.length > 0; | |
if (extras.getGlobalState().computationDepth > 0 && hasObservers) { | |
throw new Error("Computed values are not allowed to cause side effects by changing observables that are already being observed. Tried to modify: " + administration.name); | |
} | |
if (!extras.getGlobalState().allowStateChanges && hasObservers) { | |
throw new Error((extras.getGlobalState().strictMode? | |
"Since strict-mode is enabled, changing observed observable values outside actions is not allowed. Please wrap the code in an `action` if this change is intended. Tried to modify: " | |
: | |
"Side effects like changing state are not allowed at this point. Are you trying to modify state from, for example, the render function of a React component? Tried to modify: ") | |
+ administration.name); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as mobx from 'mobx'; | |
import { isPlainObject } from './index'; | |
function cache(source, detectCycles, alreadySeen, value) { | |
if (detectCycles) { | |
alreadySeen.set(source, value); | |
} | |
return value; | |
} | |
export function toJS(source, detectCycles = true, alreadySeen = new Map()) { | |
if (mobx.isObservable(source)) { | |
if (detectCycles && source !== null && typeof source === "object") { | |
if (alreadySeen.has(source)) { | |
return alreadySeen.get(source); | |
} | |
} | |
if (source instanceof Set) { | |
const res = cache(source, detectCycles, alreadySeen, new Set()); | |
source.forEach(function (value) { | |
res.add(toJS(value, detectCycles, alreadySeen)); | |
}); | |
return res; | |
} | |
if (Array.isArray(source) || mobx.isObservableArray(source)) { | |
return cache(source, detectCycles, alreadySeen, source.map(function (value) { return toJS(value, detectCycles, alreadySeen); })); | |
} | |
if (source instanceof Map || mobx.isObservableMap(source)) { | |
const res = cache(source, detectCycles, alreadySeen, new Map()); | |
source.forEach(function (value, key) { | |
res.set(key, toJS(value, detectCycles, alreadySeen)); | |
}); | |
return res; | |
} | |
if (isPlainObject(source) || mobx.isObservableObject(source)) { | |
const res = cache(source, detectCycles, alreadySeen, {}); | |
for (let key of Object.keys(source)) { | |
res[key] = toJS(source[key], detectCycles, alreadySeen); | |
} | |
return res; | |
} | |
if (mobx.isObservableValue(source)) { | |
return toJS(source.get(), detectCycles, alreadySeen); | |
} | |
} | |
return source; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Atom, | |
extras, | |
transaction | |
} from 'mobx'; | |
import { | |
checkIfStateModificationsAreAllowed, | |
hasInterceptors, | |
interceptChange, | |
notifyListeners | |
} from "./index"; | |
export function getDataAtom(key) { | |
let atom = this.get(key); | |
if (atom === undefined) { | |
atom = this.createDataAtom(key); | |
} | |
return atom; | |
} | |
export class Administration extends Atom { | |
constructor(name, enhancer) { | |
super(name); | |
this.enhancer = enhancer; | |
return this; | |
} | |
} | |
export class CollectionAdministration extends Administration { | |
constructor(store, name, enhancer, createAtom, observe, intercept) { | |
super(name, enhancer); | |
this.atoms = new Map(); | |
this.size = new Atom(); | |
this.any = new Atom(); | |
this.atoms.createDataAtom = createAtom.bind(this.atoms); | |
this.getDataAtom = getDataAtom.bind(this.atoms); | |
this.intercept = intercept; | |
this.observe = observe; | |
store.$mobx = this; | |
return this; | |
} | |
} | |
export function startReport(administration, change) { | |
if (extras.isSpyEnabled()) { | |
extras.spyReportStart(change); | |
} | |
} | |
export function endReport(administration, change) { | |
notifyListeners(administration, change); | |
if (extras.isSpyEnabled()) { | |
extras.spyReportEnd(); | |
} | |
} | |
export function report(change, administration, reporter) { | |
startReport(administration, change); | |
transaction(reporter); | |
endReport(administration, change); | |
} | |
export function prepareChange(change, administration, enhancer, newValue) { | |
if (hasInterceptors(administration)) { | |
change = interceptChange(administration, change); | |
} | |
if (change) { | |
change[newValue] = enhancer(change[newValue]); | |
} | |
return change; | |
} | |
export function startChange(administration) { | |
checkIfStateModificationsAreAllowed(administration); | |
return administration; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export * from './types-map-index'; | |
export * from './types-administration'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
Atom | |
} from 'mobx'; | |
export function createDataAtom(key) { | |
let observed = 0; | |
const dataAtom = { }; | |
const onObserve = function() { | |
++observed; | |
}; | |
const onNoObserve = () => { | |
if (--observed === 0) { | |
this.delete(key); | |
} | |
}; | |
dataAtom.has = new Atom(undefined, onObserve, onNoObserve); | |
dataAtom.write = new Atom(undefined, onObserve, onNoObserve); | |
this.set(key, dataAtom); | |
return dataAtom; | |
} | |
function reportOther(administration, key) { | |
const atom = administration.atoms.get(key); | |
if (atom !== undefined) { | |
atom.has.reportChanged(); | |
} | |
administration.size.reportChanged(); | |
if (atom !== undefined) { | |
atom.write.reportChanged(); | |
} | |
administration.any.reportChanged(); | |
} | |
export function reportUpdate(administration, key) { | |
const atom = administration.atoms.get(key); | |
if (atom !== undefined) { | |
atom.write.reportChanged(); | |
} | |
administration.any.reportChanged(); | |
} | |
export const reportAdd = reportOther; | |
export const reportDelete = reportOther; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export * from './types-map-observable-map'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
extras, | |
isObservable, | |
isObservableMap, | |
isObservableArray, | |
isObservableObject | |
} from 'mobx'; | |
import { | |
isPlainObject, | |
deepEnhancer, | |
referenceEnhancer, | |
CollectionAdministration | |
} from './index'; | |
import { | |
createDataAtom | |
} from './types-map-administration'; | |
import * as operation from './types-map-operation'; | |
const TYPE = 'ObservableMap'; | |
export function observableMap(inputReference, enhancer, name) { | |
[inputReference, enhancer, name] = mapArguments(inputReference, enhancer, name, TYPE); | |
const reference = enhanceMap(toMap(inputReference), enhancer); | |
if (reference === inputReference && isObservable(inputReference)) { | |
return reference; | |
} | |
// stores the original reference and its operations | |
// toString and clear are not included as they are going to be completely | |
// overwritten | |
const store = { }; | |
store.reference = reference; | |
store.size = reference.size; | |
store.delete = reference.delete.bind(reference); | |
store.set = reference.set.bind(reference); | |
store.entries = reference.entries.bind(reference); | |
store.forEach = reference.forEach.bind(reference); | |
store.get = reference.get.bind(reference); | |
store.has = reference.has.bind(reference); | |
store.keys = reference.keys.bind(reference); | |
store.values = reference.values.bind(reference); | |
store[Symbol.iterator] = reference[Symbol.iterator].bind(reference); | |
// override the reference with reportable operations | |
// all reportable operations reference original operations, thus they need | |
// to be bound to the store | |
const reportedStore = { }; | |
reference.delete = operation.$delete.bind(store); | |
reference.set = operation.$set.bind(store); | |
reference.entries = operation.$entries.bind(store); | |
reference.forEach = operation.$forEach.bind(store); | |
reference.get = operation.$get.bind(store); | |
reference.has = operation.$has.bind(store); | |
reference.keys = operation.$keys.bind(store); | |
reference.values = operation.$values.bind(store); | |
reference[Symbol.iterator] = operation.$symbolIterator.bind(store); | |
reference.toString = operation.$toString.bind(store); | |
reference.clear = operation.$clear.bind(store); | |
Object.defineProperty(reference, 'size', { | |
get: operation.$getSize.bind(store), | |
enumerable: true, | |
configurable: true | |
}); | |
// enhance the reference with standard mobx operations | |
reference.toJS = operation.$toJS.bind(store); | |
reference.toJSON = operation.$toJSON.bind(store); | |
reference.replace = operation.$replace.bind(store); | |
reference.merge = operation.$merge.bind(store); | |
reference.observe = operation.$observe.bind(store); | |
reference.intercept = operation.$intercept.bind(store); | |
// hook reporting in | |
// the original store needs to have the reporting since all reported operations | |
// point back to it | |
const administration = new CollectionAdministration( | |
store, | |
name, | |
enhancer, | |
createDataAtom, | |
reference.observe, // getAdministration support | |
reference.intercept // getAdministration support | |
); | |
Object.defineProperty(reference, '$mobx', { | |
value: administration, | |
writable: false, | |
enumerable: true, | |
configurable: true | |
}); | |
return reference; | |
} | |
export function shallowObservableMap(reference, name) { | |
return observableMap(reference, referenceEnhancer, name); | |
} | |
function toMap(value) { | |
if (value === null || value === undefined) { | |
return new Map(); | |
} | |
if (value instanceof Map || isObservableMap(value)) { | |
return value; | |
} | |
if (Array.isArray(value) || isObservableArray(value)) { | |
return new Map(value); | |
} | |
if (isPlainObject(value) || isObservableObject(value)) { | |
return new Map(Array.from(Object.entries(value))); | |
} | |
throw new Error('Cannot initialize map from ' + value); | |
} | |
function enhanceMap(map, enhancer) { | |
if (isObservable(map)) { | |
return map; | |
} | |
for (let entry of map.entries()) { | |
map.set(entry[0], enhancer(entry[1])); | |
} | |
return map; | |
} | |
function mapArguments(reference, enhancer, name, type) { | |
// figure out which type of overload it is and do validation | |
if (typeof reference === 'function') { | |
// (enhancer, name?) | |
if (name !== undefined) { | |
// ERROR 1 | |
throw new Error('(enhancer, name?) too many arguments supplied to function (expecting 2, got 3)'); | |
} | |
name = enhancer; | |
enhancer = reference; | |
reference = undefined; | |
if (name !== undefined && name !== null && typeof name !== 'string') { | |
// ERROR 2 | |
throw new Error('(enhancer, name?) invalid argument supplied: name must be a string { name: ' + name + '}'); | |
} | |
} | |
else if (typeof reference === 'string') { | |
// (name) | |
const count = name !== undefined? 3 : enhancer !== undefined? 2 : 0; | |
if (count !== 0) { | |
// ERROR 3 | |
throw new Error('(name) too many arguments supplied to function (expecting 1, got ' + count + ')'); | |
} | |
name = reference; | |
reference = undefined; | |
enhancer = undefined; | |
} | |
else if (typeof enhancer === 'string') { | |
// (reference, name?) | |
if (name !== undefined) { | |
// ERROR 4 | |
throw new Error('(reference, name?) too many arguments supplied to function (expecting 2, got 3)'); | |
} | |
name = enhancer; | |
enhancer = undefined; | |
if (reference !== undefined && reference !== null && typeof reference !== 'object') { | |
// ERROR 5 | |
throw new Error('(reference, name?) invalid argument supplied: reference must be an object capable of initializing a map { reference: ' + reference + '}'); | |
} | |
} | |
else if (reference !== undefined && typeof reference !== 'object') { | |
// ERROR 6 | |
throw new Error('(unknown) invalid argument supplied: reference must be an object capable of initializing a map { reference: ' + reference + '}'); | |
} | |
enhancer = enhancer === undefined? deepEnhancer : enhancer; | |
name = name === undefined? type + '@' + (++extras.getGlobalState().mobxGuid) : name; | |
return [reference, enhancer, name]; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as mobx from 'mobx'; | |
import { | |
toJS, | |
observableMap, | |
shallowObservableMap | |
} from './index'; | |
const autorun = mobx.autorun; | |
const map = (reference, enhancer, name) => { | |
return observableMap(reference, enhancer, name); | |
}; | |
const shallowMap = (reference, name) => { | |
return shallowObservableMap(reference, name); | |
}; | |
describe('#Observable Map', () => { | |
it ('is observable and is map', function() { | |
let m = map(); | |
expect(mobx.isObservable(m)).toBe(true); | |
expect(m instanceof Map).toBe(true); | |
expect(mobx.isObservableMap(m)).toBe(false); | |
}); | |
it('intercept blocking', function() { | |
const m = map(); | |
m.set('a', 5); | |
let d = mobx.intercept(m, function(change) { | |
if (change.type === 'update') { | |
return undefined; | |
} | |
return change; | |
}); | |
m.set('a', 6); | |
expect(m.get('a')).toBe(5); | |
d(); | |
m.set('a', 7); | |
expect(m.get('a')).toBe(7); | |
d = mobx.intercept(m, function(change) { | |
if (change.type === 'add') { | |
return undefined; | |
} | |
return change; | |
}); | |
m.set('b', 12); | |
expect(m.has('b')).toBe(false); | |
d(); | |
m.set('b', 9); | |
expect(m.get('b')).toBe(9); | |
d = mobx.intercept(m, function(change) { | |
if (change.type === 'delete') { | |
return undefined; | |
} | |
return change; | |
}); | |
m.delete('b'); | |
expect(m.get('b')).toBe(9); | |
d(); | |
m.delete('b'); | |
expect(m.has('b')).toBe(false); | |
d = mobx.intercept(m, function(change) { | |
if (change.type === 'add') { | |
return undefined; | |
} | |
return change; | |
}); | |
let d2 = mobx.intercept(m, function(change) { | |
if (change.type === 'update') { | |
return undefined; | |
} | |
return change; | |
}); | |
let d3 = mobx.intercept(m, function(change) { | |
if (change.type === 'delete') { | |
return undefined; | |
} | |
return change; | |
}); | |
m.set('a', 6); | |
expect(m.get('a')).toBe(7); | |
m.set('b', 6); | |
expect(m.has('b')).toBe(false); | |
m.delete('a'); | |
expect(m.has('a')).toBe(true); | |
d(); | |
d2(); | |
d3(); | |
m.set('a', 6); | |
expect(m.get('a')).toBe(6); | |
m.set('b', 6); | |
expect(m.get('b')).toBe(6); | |
m.delete('a'); | |
expect(m.has('a')).toBe(false); | |
d = mobx.intercept(m, function(change) { | |
if (change.type === 'add') { | |
change.newValue = undefined; | |
} | |
return change; | |
}); | |
m.set('a', 6); | |
expect(m.has('a')).toBe(true); | |
expect(m.get('a')).toBe(undefined); | |
d(); | |
d = mobx.intercept(m, function(change) { | |
if (change.type === 'delete') { | |
change.newValue = 24; | |
} | |
return change; | |
}); | |
m.delete('a'); | |
expect(m.has('a')).toBe(false); | |
d(); | |
d = mobx.intercept(m, function(change) { | |
if (change.type === 'add') { | |
change.newValue = 44; | |
} | |
return change; | |
}); | |
m.set('a', 6); | |
expect(m.has('a')).toBe(true); | |
expect(m.get('a')).toBe(44); | |
d(); | |
d = mobx.intercept(m, function(change) { | |
if (change.type === 'update') { | |
change.newValue = 31; | |
} | |
return change; | |
}); | |
m.set('a', 6); | |
expect(m.has('a')).toBe(true); | |
expect(m.get('a')).toBe(31); | |
d(); | |
}); | |
it ('an observable map shouldn\'t be remade', function() { | |
const m1 = map(); | |
const a1 = m1.$mobx; | |
const m2 = map(m1); | |
const a2 = m2.$mobx; | |
expect(m1).toBe(m2); | |
expect(a1).toBe(a2); | |
}); | |
it ('make maps observable without copying', function() { | |
let source = new Map(); | |
let m = map(source); | |
expect(m).toBe(source); | |
}); | |
it ('keys don\'t become observable', function() { | |
let m = map([[{a: 7}, 9]]); | |
let o = { b: 9 }; | |
m.set(o, 7); | |
let keys = [...m.keys()]; | |
expect(keys[0].a).toBe(7); | |
expect(keys[1].b).toBe(9); | |
for (let key of keys) { | |
expect (mobx.isObservable(key)).toBe(false); | |
} | |
}); | |
it ('no atom without reaction', function() { | |
let m = map(); | |
let d = autorun(() => { | |
let x; | |
m.toJS(); | |
m.toJSON(); | |
m.forEach(() => { }); | |
x = [...m.entries()]; | |
x = [...m.values()]; | |
x = [...m.keys()]; | |
x = [...m]; | |
}); | |
mobx.transaction(() => { | |
m.has('b'); | |
expect(m.$mobx.atoms.has('b')).toBe(false); | |
m.set('b', 5); | |
expect(m.$mobx.atoms.has('b')).toBe(false); | |
m.get('b'); | |
expect(m.$mobx.atoms.has('b')).toBe(false); | |
m.delete('b'); | |
expect(m.$mobx.atoms.has('b')).toBe(false); | |
}); | |
expect(m.$mobx.atoms.has('b')).toBe(false); | |
d(); | |
}); | |
it ('atom with reaction + cleanup', function() { | |
let m = map(); | |
let d = autorun(() => { | |
m.get('b'); | |
}); | |
expect(m.$mobx.atoms.has('b')).toBe(true); | |
m.has('b'); | |
expect(m.$mobx.atoms.has('b')).toBe(true); | |
m.set('b', 5); | |
expect(m.$mobx.atoms.has('b')).toBe(true); | |
m.get('b'); | |
expect(m.$mobx.atoms.has('b')).toBe(true); | |
m.delete('b'); | |
expect(m.$mobx.atoms.has('b')).toBe(true); | |
d(); | |
expect(m.$mobx.atoms.has('b')).toBe(false); | |
}); | |
it ('atom changing', function() { | |
let m = map(); | |
let c = mobx.observable('b'); | |
let d = autorun(() => { | |
m.get(c.get()); | |
}); | |
expect(m.$mobx.atoms.has('b')).toBe(true); | |
expect(m.$mobx.atoms.has('c')).toBe(false); | |
c.set('c'); | |
expect(m.$mobx.atoms.has('b')).toBe(false); | |
expect(m.$mobx.atoms.has('c')).toBe(true); | |
d(); | |
expect(m.$mobx.atoms.has('b')).toBe(false); | |
expect(m.$mobx.atoms.has('c')).toBe(false); | |
}); | |
function mapCrud(m, events, events2) { | |
expect(m.has("a")).toBe(true); | |
expect(m.has("b")).toBe(false); | |
expect(m.get("a")).toBe(1); | |
expect(m.get("b")).toBe(undefined); | |
expect(m.size).toBe(1); | |
m.set("a", 2); | |
expect(m.has("a")).toBe(true); | |
expect(m.get("a")).toBe(2); | |
m.set("b", 3); | |
expect(m.has("b")).toBe(true); | |
expect(m.get("b")).toBe(3); | |
m.set("b", 3); | |
expect(m.has("b")).toBe(true); | |
expect(m.get("b")).toBe(3); | |
expect([...m.keys()]).toEqual(["a", "b"]); | |
expect([...m.values()]).toEqual([2, 3]); | |
expect([...m.entries()]).toEqual([["a", 2], ["b", 3]]); | |
expect([...m]).toEqual([["a", 2], ["b", 3]]); | |
expect(m.toJSON()).toEqual({"a":2,"b":3}); | |
expect(JSON.stringify(m.toJSON())).toEqual('{"a":2,"b":3}'); | |
expect(m.toString()).toEqual("ObservableMap@1[{ a: 2, b: 3 }]"); | |
expect(m.size).toBe(2); | |
m.clear(); | |
expect([...m.keys()]).toEqual([]); | |
expect([...m.values()]).toEqual([]); | |
expect([...m]).toEqual([]); | |
expect(m.toJSON()).toEqual({}); | |
expect(m.toString()).toEqual("ObservableMap@1[{ }]"); | |
expect(m.size).toBe(0); | |
expect(m.has("a")).toBe(false); | |
expect(m.has("b")).toBe(false); | |
expect(m.get("a")).toBe(undefined); | |
expect(m.get("b")).toBe(undefined); | |
function removeObjectProp(item) { | |
delete item.object; | |
return item | |
} | |
expect(events.map(removeObjectProp)).toEqual([ | |
{ | |
reference: m, | |
type: "update", | |
name: "a", | |
oldValue: 1, | |
newValue: 2 | |
}, | |
{ | |
reference: m, | |
type: "add", | |
name: "b", | |
newValue: 3 | |
}, | |
{ | |
reference: m, | |
type: "delete", | |
name: "a", | |
oldValue: 2 | |
}, | |
{ | |
reference: m, | |
type: "delete", | |
name: "b", | |
oldValue: 3 | |
} | |
]); | |
expect(events2.map(removeObjectProp)).toEqual([ | |
{ | |
reference: m, | |
type: "update", | |
name: "a", | |
oldValue: 1, | |
newValue: 2 | |
}, | |
{ | |
reference: m, | |
type: "add", | |
name: "b", | |
newValue: 3 | |
}, | |
{ | |
reference: m, | |
type: "update", | |
name: "b", | |
newValue: 3, | |
oldValue: 3 | |
}, | |
{ | |
reference: m, | |
type: "delete", | |
name: "a", | |
oldValue: 2 | |
}, | |
{ | |
reference: m, | |
type: "delete", | |
name: "b", | |
oldValue: 3 | |
} | |
]); | |
} | |
it("map crud", function() { | |
mobx.extras.getGlobalState().mobxGuid = 0; // hmm dangerous reset? | |
let events = []; | |
let events2 = []; | |
let m = map({ a: 1 }); | |
m.observe(function(changes) { | |
events.push(changes); | |
}); | |
m.intercept(function(changes) { | |
events2.push(changes); | |
return changes; | |
}); | |
mapCrud(m, events, events2); | |
}); | |
it("map crud 2", function() { | |
mobx.extras.getGlobalState().mobxGuid = 0; // hmm dangerous reset? | |
let events = []; | |
let events2 = []; | |
let m = map({ a: 1 }); | |
mobx.observe(m, function(changes) { | |
events.push(changes); | |
}); | |
mobx.intercept(m, function(changes) { | |
events2.push(changes); | |
return changes; | |
}); | |
mapCrud(m, events, events2); | |
}); | |
it("map merge", function() { | |
let a = map({ a: 1, b: 2, c: 2 }); | |
let b = map({ c: 3, d: 4 }); | |
a.merge(b); | |
const merged = new Map(); | |
merged.set('a', 1); | |
merged.set('b', 2); | |
merged.set('c', 3); | |
merged.set('d', 4); | |
expect(a).toEqual(merged); | |
a = map({ a: 1, b: 2, c: 2 }); | |
b = [['c', 3], ['d', 4]]; | |
a.merge(b); | |
expect(a).toEqual(merged); | |
a = map({ a: 1, b: 2, c: 2 }); | |
b = { c: 3, d: 4 }; | |
a.merge(b); | |
expect(a).toEqual(merged); | |
a.merge(); | |
expect(a).toEqual(merged); | |
a.merge(null); | |
expect(a).toEqual(merged); | |
expect(function() { | |
a.merge(new Set()); | |
}).toThrow(); | |
expect(function() { | |
a.merge(5); | |
}).toThrow(); | |
expect(function() { | |
a.merge(''); | |
}).toThrow(); | |
}); | |
it('map observe fire immediately', function() { | |
const m = map({a: 5, c: 8}); | |
const events = []; | |
m.observe(function(change) { | |
events.push(change); | |
}, true); | |
m.intercept(function(change) { | |
events.push(change); | |
return change; | |
}, true); | |
expect(events).toEqual([ | |
{ | |
reference: m, | |
type: 'add', | |
name: 'a', | |
newValue: 5 | |
}, | |
{ | |
reference: m, | |
type: 'add', | |
name: 'c', | |
newValue: 8 | |
} | |
]); | |
}); | |
it("observe value", function() { | |
let a = map(); | |
let hasX = false; | |
let valueX = undefined; | |
let valueY = undefined; | |
autorun(function() { | |
hasX = a.has("x") | |
}); | |
autorun(function() { | |
valueX = a.get("x") | |
}); | |
autorun(function() { | |
valueY = a.get("y") | |
}); | |
expect(hasX).toBe(false); | |
expect(valueX).toBe(undefined); | |
a.set("x", 3); | |
expect(hasX).toBe(true); | |
expect(valueX).toBe(3); | |
a.set("x", 4); | |
expect(hasX).toBe(true); | |
expect(valueX).toBe(4); | |
a.delete("x"); | |
expect(hasX).toBe(false); | |
expect(valueX).toBe(undefined); | |
a.set("x", 5); | |
expect(hasX).toBe(true); | |
expect(valueX).toBe(5); | |
expect(valueY).toBe(undefined); | |
a.merge({ y: "hi" }); | |
expect(valueY).toBe("hi"); | |
a.merge({ y: "hello" }); | |
expect(valueY).toBe("hello"); | |
a.replace({ y: "stuff", z: "zoef" }); | |
expect(valueY).toBe("stuff"); | |
expect([...a.keys()]).toEqual(["y", "z"]); | |
a.replace(); | |
expect(valueY).toBe(undefined); | |
expect([...a.keys()]).toEqual([]); | |
a.replace([['y', 'stuff'], ['z', 'zoef']]); | |
expect(valueY).toBe("stuff"); | |
expect([...a.keys()]).toEqual(["y", "z"]); | |
a.replace([['x', 'stuff'], ['f', 'zoef']]); | |
expect(valueX).toBe("stuff"); | |
expect([...a.keys()]).toEqual(["x", "f"]); | |
a.replace(new Map([['y', 'stuff'], ['z', 'zoef']])); | |
expect(valueY).toBe("stuff"); | |
expect([...a.keys()]).toEqual(["y", "z"]); | |
}); | |
it("initialize with entries", function() { | |
let a = map([["a", 1], ["b", 2]]); | |
expect([...a.toJS()]).toEqual([["a", 1], ["b", 2]]); | |
}); | |
it("initialize with empty value", function() { | |
let a = map(); | |
let b = map({}); | |
let c = map([]); | |
let d = map(new Map()); | |
a.set("0", 0); | |
b.set("0", 0); | |
c.set("0", 0); | |
d.set("0", 0); | |
const emptyMap = new Map([['0', 0]]); | |
expect(a.toJS()).toEqual(emptyMap); | |
expect(b.toJS()).toEqual(emptyMap); | |
expect(c.toJS()).toEqual(emptyMap); | |
expect(d.toJS()).toEqual(emptyMap); | |
}); | |
it("observe collections", function() { | |
let x = map(); | |
let keys, values, entries; | |
autorun(function() { | |
keys = [...x.keys()]; | |
}); | |
autorun(function() { | |
values = [...x.values()]; | |
}); | |
autorun(function() { | |
entries = [...x.entries()]; | |
}); | |
x.set("a", 1); | |
expect(keys).toEqual(["a"]); | |
expect(values).toEqual([1]); | |
expect(entries).toEqual([["a", 1]]); | |
// should not retrigger: | |
keys = null; | |
values = null; | |
entries = null; | |
x.set("a", 1); | |
expect(keys).toEqual(null); | |
expect(values).toEqual(null); | |
expect(entries).toEqual(null); | |
x.set("a", 2); | |
expect(values).toEqual([2]); | |
expect(entries).toEqual([["a", 2]]); | |
x.set("b", 3); | |
expect(keys).toEqual(["a", "b"]); | |
expect(values).toEqual([2, 3]); | |
expect(entries).toEqual([["a", 2], ["b", 3]]); | |
x.has("c"); | |
expect(keys).toEqual(["a", "b"]); | |
expect(values).toEqual([2, 3]); | |
expect(entries).toEqual([["a", 2], ["b", 3]]); | |
x.delete("a"); | |
expect(keys).toEqual(["b"]); | |
expect(values).toEqual([3]); | |
expect(entries).toEqual([["b", 3]]); | |
}); | |
it("cleanup", function() { | |
let x = map({ a: 1 }); | |
let aValue; | |
let disposer = autorun(function() { | |
aValue = x.get('a'); | |
}); | |
const atom = x.$mobx.atoms.get('a'); | |
let observable = atom.write; | |
let observableHas = atom.has; | |
expect(aValue).toBe(1); | |
expect(observable.observers.length).toBe(1); | |
expect(observableHas.observers.length).toBe(1); | |
expect(x.$mobx.atoms.has('a')).toBe(true); | |
expect(x.delete("a")).toBe(true); | |
expect(x.delete("not-existing")).toBe(false); | |
expect(aValue).toBe(undefined); | |
expect(observable.observers.length).toBe(0); | |
expect(observableHas.observers.length).toBe(1); | |
expect(x.$mobx.atoms.has('a')).toBe(true); | |
x.set("a", 2); | |
expect(aValue).toBe(2); | |
expect(observable.observers.length).toBe(1); | |
expect(observableHas.observers.length).toBe(1); | |
expect(x.$mobx.atoms.has('a')).toBe(true); | |
disposer(); | |
expect(aValue).toBe(2); | |
expect(observable.observers.length).toBe(0); | |
expect(observableHas.observers.length).toBe(0); | |
expect(x.$mobx.atoms.has('a')).toBe(false); | |
}); | |
it("strict", function() { | |
mobx.useStrict(true); | |
let x = map(); | |
autorun(function() { | |
x.get("y") // should not throw | |
}); | |
mobx.useStrict(false); | |
}); | |
it("issue 100", function() { | |
let that = {}; | |
mobx.extendObservable(that, { | |
myMap: map() | |
}); | |
expect(mobx.isObservable(that.myMap) && that.myMap instanceof Map).toBe(true); | |
expect(typeof that.myMap.observe).toBe("function"); | |
}); | |
it("issue 119 - unobserve before delete", function() { | |
let propValues = []; | |
let myObservable = mobx.observable({ | |
myMap: map() | |
}); | |
myObservable.myMap.set("myId", { | |
myProp: "myPropValue", | |
myCalculatedProp: mobx.computed(function() { | |
if (myObservable.myMap.has("myId")) | |
return myObservable.myMap.get("myId").myProp + " calculated"; | |
return undefined | |
}) | |
}); | |
// the error only happens if the value is observed | |
mobx.autorun(function() { | |
[...myObservable.myMap.values()].forEach(function(value) { | |
console.log("x"); | |
propValues.push(value.myCalculatedProp) | |
}) | |
}); | |
myObservable.myMap.delete("myId"); | |
expect(propValues).toEqual(["myPropValue calculated"]); | |
}); | |
it("issue 116 - has should not throw on abnormal keys", function() { | |
let x = map(); | |
expect(x.has(undefined)).toBe(false); | |
expect(x.has(null)).toBe(false); | |
expect(x.has({})).toBe(false); | |
expect(x.has([])).toBe(false); | |
expect(x.get(undefined)).toBe(undefined); | |
expect(x.get(null)).toBe(undefined); | |
expect(x.get({})).toBe(undefined); | |
expect(x.get([])).toBe(undefined); | |
expect(function() { | |
x.set(undefined, true); | |
x.set(null, true); | |
x.set({}, true); | |
x.set([], true); | |
}).not.toThrow(); | |
expect(function() { | |
map(5); | |
}).toThrow(); | |
expect(function() { | |
map(new Set()); | |
}).toThrow(); | |
expect(function() { | |
map(null); | |
}).not.toThrow(); | |
}); | |
it('function overloading', function() { | |
expect(function() { | |
const name = 'custom name'; | |
const enhance = function(ref) { return ref; }; | |
const reference = [[1,1]]; | |
const x = map(name); | |
expect(x.$mobx.name).toBe(name); | |
}).not.toThrow(); | |
expect(function() { | |
const name = 'custom name'; | |
const enhance = function(ref) { return ref; }; | |
const reference = [[1,1]]; | |
const x = map(enhance); | |
expect(x.$mobx.enhancer).toBe(enhance); | |
}).not.toThrow(); | |
expect(function() { | |
const name = 'custom name'; | |
const enhance = function(ref) { return ref; }; | |
const reference = [[1,1]]; | |
const x = map(enhance, name); | |
expect(x.$mobx.name).toBe(name); | |
expect(x.$mobx.enhancer).toBe(enhance); | |
}).not.toThrow(); | |
expect(function() { | |
const name = 'custom name'; | |
const enhance = function(ref) { return ref; }; | |
const reference = [[1,1]]; | |
const x = map(reference, name); | |
expect(x.$mobx.name).toBe(name); | |
}).not.toThrow(); | |
// ERROR 1 | |
expect(function() { | |
const name = 'custom name'; | |
const enhance = function(ref) { return ref; }; | |
const reference = [[1,1]]; | |
const x = map(enhance, reference, name); | |
}).toThrow('(enhancer, name?) too many arguments supplied to function (expecting 2, got 3)'); | |
// ERROR 2 | |
expect(function() { | |
const name = 'custom name'; | |
const enhance = function(ref) { return ref; }; | |
const reference = [[1,1]]; | |
const x = map(enhance, reference); | |
}).toThrow('(enhancer, name?) invalid argument supplied: name must be a string { name: 1,1}'); | |
// ERROR 3 | |
expect(function() { | |
const name = 'custom name'; | |
const enhance = function(ref) { return ref; }; | |
const reference = [[1,1]]; | |
const x = map(name, reference, enhance); | |
}).toThrow('(name) too many arguments supplied to function (expecting 1, got 3)'); | |
// ERROR 3 | |
expect(function() { | |
const name = 'custom name'; | |
const enhance = function(ref) { return ref; }; | |
const reference = [[1,1]]; | |
const x = map(name, reference); | |
}).toThrow('(name) too many arguments supplied to function (expecting 1, got 2)'); | |
// ERROR 4 | |
expect(function() { | |
const name = 'custom name'; | |
const enhance = function(ref) { return ref; }; | |
const reference = [[1,1]]; | |
const x = map(reference, name, enhance); | |
}).toThrow('(reference, name?) too many arguments supplied to function (expecting 2, got 3)'); | |
// ERROR 5 | |
expect(function() { | |
const name = 'custom name'; | |
const enhance = function(ref) { return ref; }; | |
const reference = [[1,1]]; | |
const x = map(1, name); | |
}).toThrow('(reference, name?) invalid argument supplied: reference must be an object capable of initializing a map'); | |
// ERROR 6 | |
expect(function() { | |
const name = 'custom name'; | |
const enhance = function(ref) { return ref; }; | |
const reference = [[1,1]]; | |
const x = map(1); | |
}).toThrow('(unknown) invalid argument supplied: reference must be an object capable of initializing a map'); | |
}); | |
it("map modifier", () => { | |
let x = map({ a: 1 }); | |
expect(x instanceof Map && mobx.isObservable(x)).toBe(true); | |
expect(x.get("a")).toBe(1); | |
x.set("b", {}); | |
expect(mobx.isObservableObject(x.get("b"))).toBe(true); | |
x = map([["a", 1]]); | |
expect(x instanceof Map && mobx.isObservable(x)).toBe(true); | |
expect(x.get("a")).toBe(1); | |
x = map(); | |
expect(x instanceof Map && mobx.isObservable(x)).toBe(true); | |
expect([...x.keys()]).toEqual([]); | |
x = mobx.observable({ a: map({ b: { c: 3 } }) }); | |
expect(mobx.isObservableObject(x)).toBe(true); | |
expect(mobx.isObservableObject(x.a)).toBe(false); | |
expect(mobx.isObservable(x.a) && x.a instanceof Map).toBe(true); | |
expect(mobx.isObservableObject(x.a.get("b"))).toBe(true); | |
}); | |
it("map modifier with modifier", () => { | |
var x = map({ a: { c: 3 } }); | |
expect(mobx.isObservableObject(x.get("a"))).toBe(true); | |
x.set("b", { d: 4 }); | |
expect(mobx.isObservableObject(x.get("b"))).toBe(true); | |
x = shallowMap({ a: { c: 3 } }); | |
expect(mobx.isObservableObject(x.get("a"))).toBe(false); | |
x.set("b", { d: 4 }); | |
expect(mobx.isObservableObject(x.get("b"))).toBe(false); | |
x = mobx.observable({ a: shallowMap({ b: {} }) }); | |
expect(mobx.isObservableObject(x)).toBe(true); | |
expect(mobx.isObservable(x.a) && x.a instanceof Map).toBe(true); | |
expect(mobx.isObservableObject(x.a.get("b"))).toBe(false); | |
x.a.set("e", {}); | |
expect(mobx.isObservableObject(x.a.get("e"))).toBe(false); | |
}); | |
it("256, map.clear should not be tracked", () => { | |
let x = observableMap({ a: 3 }); | |
let c = 0; | |
let d = mobx.autorun(() => { | |
c++; | |
x.clear(); | |
}); | |
expect(c).toBe(1); | |
x.set("b", 3); | |
expect(c).toBe(1); | |
d(); | |
}); | |
it("256, map.merge should be not be tracked for target", () => { | |
let x = map({ a: 3 }); | |
let y = map({ b: 3 }); | |
let c = 0; | |
let xatom = x._anyAtom; | |
let yatom = y._anyAtom; | |
let zz = 5; | |
let d = mobx.autorun(() => { | |
c++; | |
x.merge(y) | |
}); | |
expect(c).toBe(1); | |
expect([...x.keys()]).toEqual(["a", "b"]); | |
y.set("c", 4); | |
expect(c).toBe(2); | |
expect([...x.keys()]).toEqual(["a", "b", "c"]); | |
x.set("d", 5); | |
expect(c).toBe(2); | |
expect([...x.keys()]).toEqual(["a", "b", "c", "d"]); | |
d(); | |
}); | |
it("308, map keys should be coerced to strings correctly", () => { | |
let m = map(); | |
m.set(1, true); // => "[map { 1: true }]" | |
m.delete(1); // => "[map { }]" | |
expect([...m.keys()]).toEqual([]); | |
m.set(1, true); // => "[map { 1: true }]" | |
m.delete("1"); // => "[map { 1: undefined }]" | |
expect([...m.keys()]).toEqual([1]); | |
m.set(1, true); // => "[map { 1: true, 1: true }]" | |
m.delete("1"); // => "[map { 1: undefined, 1: undefined }]" | |
expect([...m.keys()]).toEqual([1]); | |
m.set(true, true); | |
expect(m.get("true")).toBe(undefined); | |
m.delete(true); | |
expect([...m.keys()]).toEqual([1]); | |
}); | |
/* | |
it("map should support iterall / iterable ", () => { | |
let a = map({ a: 1, b: 2 }); | |
//let a = new Map([['a', 1], ['b', 2]]); | |
//let a = [1, 2, 3]; | |
function leech(iter) { | |
let values = []; | |
let v; | |
do { | |
v = iter.next(); | |
if (!v.done) values.push(v.value) | |
} while (!v.done); | |
return values; | |
} | |
//expect(iterall.isIterable(a)).toBe(true); | |
//expect(leech(iterall.getIterator(a))).toEqual([["a", 1], ["b", 2]]); | |
//expect(leech([...a.entries()])).toEqual([["a", 1], ["b", 2]]); | |
//expect(leech([...a.keys()])).toEqual(["a", "b"]); | |
//expect(leech([...a.values()])).toEqual([1, 2]) | |
//expect(leech(a)).toEqual([1, 2, 3]); | |
}); | |
*/ | |
it("support for ES6 Map", () => { | |
let x = new Map(); | |
x.set("x", 3); | |
x.set("y", 2); | |
let m = map(x); | |
expect(mobx.isObservable(m) && m instanceof Map).toBe(true); | |
expect([...m]).toEqual([["x", 3], ["y", 2]]); | |
let x2 = new Map(); | |
x2.set("y", 4); | |
x2.set("z", 5); | |
m.merge(x2); | |
expect(m.get("z")).toEqual(5); | |
let x3 = new Map(); | |
x3.set({ y: 2 }, { z: 4 }); | |
expect(() => { | |
shallowMap(x3); | |
}).not.toThrow(); | |
}); | |
it("deepEqual map", () => { | |
let x = new Map(); | |
x.set("x", 3); | |
x.set("y", { z: 2 }); | |
let x2 = map(); | |
x2.set("x", 3); | |
x2.set("y", { z: 3 }); | |
expect(mobx.extras.deepEqual(x, x2)).toBe(false); | |
x2.get("y").z = 2; | |
expect(mobx.extras.deepEqual(x, x2)).toBe(true); | |
x2.set("z", 1); | |
expect(mobx.extras.deepEqual(x, x2)).toBe(false); | |
x2.delete("z"); | |
expect(mobx.extras.deepEqual(x, x2)).toBe(true); | |
x2.delete("y"); | |
expect(mobx.extras.deepEqual(x, x2)).toBe(false); | |
}); | |
it("798, cannot return observable map from computed prop", () => { | |
// MWE: this is an anti pattern, yet should be possible in certain cases nonetheless..? | |
// https://jsfiddle.net/7e6Ltscr/ | |
const form = function(settings) { | |
let form = mobx.observable({ | |
reactPropsMap: map({ | |
onSubmit: function() { | |
console.log("onSubmit init!") | |
} | |
}), | |
model: { | |
value: "TEST" | |
} | |
}); | |
form.reactPropsMap.set("onSubmit", function() { | |
console.log("onSubmit overwritten!") | |
}); | |
return form; | |
}; | |
const customerSearchStore = function() { | |
let customerSearchStore = mobx.observable({ | |
customerType: "RUBY", | |
searchTypeFormStore: mobx.computed(function() { | |
return form(customerSearchStore.customerType) | |
}), | |
customerSearchType: mobx.computed(function() { | |
return form(customerSearchStore.searchTypeFormStore.model.value) | |
}) | |
}); | |
return customerSearchStore | |
}; | |
let cs = customerSearchStore(); | |
expect(() => { | |
console.log(cs.customerSearchType) | |
}).not.toThrow(); | |
}); | |
it("869, deeply observable map should make added items observables as well", () => { | |
let store = { | |
map_deep1: map(), | |
map_deep2: map() | |
}; | |
expect(mobx.isObservable(store.map_deep1) && store.map_deep1 instanceof Map).toBeTruthy(); | |
expect(mobx.isObservable(store.map_deep2) && store.map_deep2 instanceof Map).toBeTruthy(); | |
store.map_deep2.set("a", []); | |
expect(mobx.isObservable(store.map_deep2.get("a"))).toBeTruthy(); | |
store.map_deep1.set("a", []); | |
expect(mobx.isObservable(store.map_deep1.get("a"))).toBeTruthy() | |
}); | |
it("using deep map", () => { | |
let store = { | |
map_deep: map() | |
}; | |
// Creating autorun triggers one observation, hence -1 | |
let observed = -1; | |
mobx.autorun(function() { | |
// Use the map, to observe all changes | |
toJS(store.map_deep); | |
observed++; | |
}); | |
store.map_deep.set("shoes", []); | |
expect(observed).toBe(1); | |
store.map_deep.get("shoes").push({ color: "black" }); | |
expect(observed).toBe(2); | |
store.map_deep.get("shoes")[0].color = "red"; | |
expect(observed).toBe(3); | |
}); | |
it("issue 893", () => { | |
const m = map(); | |
const keys = ["constructor", "toString", "assertValidKey", "isValidKey", "toJSON", "toJS"]; | |
for (let key of keys) { | |
expect(m.get(key)).toBe(undefined); | |
} | |
}); | |
it("work with 'toString' key", () => { | |
const m = map(); | |
expect(m.get("toString")).toBe(undefined); | |
m.set("toString", "test"); | |
expect(m.get("toString")).toBe("test"); | |
}); | |
it("issue 940, should not be possible to change maps outside strict mode", () => { | |
mobx.useStrict(true); | |
const m = observableMap(); | |
const d = mobx.autorun(() => m.values()); | |
expect(() => { | |
m.set("x", 1); | |
}).toThrowError('Since strict-mode is enabled'); | |
d(); | |
mobx.useStrict(false); | |
}); | |
it("issue 1243, .replace should not trigger change on unchanged values", () => { | |
const m = map({ a: 1, b: 2, c: 3 }); | |
let recomputeCount = 0; | |
let visitedComputed = false; | |
const computedValue = mobx.computed(() => { | |
recomputeCount++; | |
return m.get("a") | |
}); | |
const d = mobx.autorun(() => { | |
computedValue.get() | |
}); | |
// recompute should happen once by now, due to the autorun | |
expect(recomputeCount).toBe(1); | |
// a hasn't changed, recompute should not happen | |
m.replace({ a: 1, d: 5 }); | |
expect(recomputeCount).toBe(1); | |
// this should cause a recompute | |
m.replace({ a: 2 }); | |
expect(recomputeCount).toBe(2); | |
// this should remove key a and cause a recompute | |
m.replace({ b: 2 }); | |
expect(recomputeCount).toBe(3); | |
m.replace([["a", 1]]); | |
expect(recomputeCount).toBe(4); | |
const nativeMap = new Map(); | |
nativeMap.set("a", 2); | |
m.replace(nativeMap); | |
expect(recomputeCount).toBe(5); | |
expect(() => { | |
m.replace("not-an-object") | |
}).toThrow(); | |
d(); | |
}); | |
it("#1258 cannot replace maps anymore", () => { | |
const items = map(); | |
items.replace(map()); | |
}); | |
it('Should not report change when there is no update', () => { | |
const map = observableMap(); | |
let ran = -1; | |
const disposer = autorun(() => { | |
++ran; | |
for (let [key, value] of map.entries()) { | |
} | |
}); | |
map.set('a', 5); | |
map.set('a', 5); | |
const obj = {}; | |
map.set('b', obj); | |
map.set('b', obj); | |
expect(ran).toBe(3); | |
disposer(); | |
}); | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
prepareChange, | |
report, | |
startChange, | |
inReaction, | |
isPlainObject, | |
registerInterceptor, | |
registerListener | |
} from './index'; | |
import { | |
transaction, | |
isObservableMap, | |
isObservableArray, | |
isObservableObject | |
} from 'mobx'; | |
import { | |
reportUpdate, | |
reportAdd, | |
reportDelete | |
} from './types-map-administration'; | |
const UPDATE = 'update'; | |
const ADD = 'add'; | |
const DELETE = 'delete'; | |
const NEW_VALUE = 'newValue'; | |
const OLD_VALUE = 'oldValue'; | |
const OPERATION_TYPE = 'type'; | |
const REFERENCE = 'reference'; | |
const NAME = 'name'; | |
// the following three functions can have their lines reduced a bit at the cost of readability | |
function createUpdateChange(store, key, value) { | |
return { | |
[REFERENCE]: store.reference, | |
[OPERATION_TYPE]: UPDATE, | |
[NAME]: key, | |
[OLD_VALUE]: store.get(key), | |
[NEW_VALUE]: value | |
}; | |
} | |
function createAddChange(store, key, value) { | |
return { | |
[REFERENCE]: store.reference, | |
[OPERATION_TYPE]: ADD, | |
[NAME]: key, | |
[NEW_VALUE]: value | |
}; | |
} | |
function createDeleteChange(store, key) { | |
return { | |
[REFERENCE]: store.reference, | |
[OPERATION_TYPE]: DELETE, | |
[NAME]: key, | |
[OLD_VALUE]: store.get(key) | |
}; | |
} | |
function prepareChangeHelper(change, administration) { | |
return prepareChange(change, administration, administration.enhancer, NEW_VALUE); | |
} | |
function reportChange(change, administration, reporter, key) { | |
report(change, administration, function() { reporter(administration, key) }); | |
} | |
export function $set(key, value) { | |
if (this.has(key)) { | |
setUpdate(this, key, value); | |
} | |
else { | |
setAdd(this, key, value); | |
} | |
return this.reference; | |
} | |
// interim overrides like setUpdate and setAdd do not and should not be thrown into any stores as they | |
// are not in any way useful and only complicate the implementation | |
// overrides should be 1:1 with the original API | |
function setUpdate(store, key, value) { | |
const administration = startChange(store.$mobx); | |
const change = prepareChangeHelper(createUpdateChange(store, key, value), administration); | |
if (!change) { | |
return false; | |
} | |
if (change[NEW_VALUE] === change[OLD_VALUE]) { | |
return true; | |
} | |
store.set(key, change[NEW_VALUE]); | |
reportChange(change, administration, reportUpdate, key); | |
return true; | |
} | |
function setAdd(store, key, value) { | |
const administration = startChange(store.$mobx); | |
const change = prepareChangeHelper(createAddChange(store, key, value), administration); | |
if (!change) { | |
return false; | |
} | |
store.set(key, change[NEW_VALUE]); | |
++store.size; | |
reportChange(change, administration, reportAdd, key); | |
return true; | |
} | |
export function $delete(key) { | |
const administration = startChange(this.$mobx); | |
if (!this.has(key)) { | |
return false; | |
} | |
const change = prepareChangeHelper(createDeleteChange(this, key), administration); | |
if (!change) { | |
return false; | |
} | |
--this.size; | |
this.delete(key); | |
reportChange(change, administration, reportDelete, key); | |
return true; | |
} | |
export function $getSize() { | |
return this.size; | |
} | |
export function $has(key) { | |
if (inReaction()) { | |
const administration = this.$mobx; | |
administration.getDataAtom(key).has.reportObserved(); | |
administration.reportObserved(); | |
} | |
return this.has(key); | |
} | |
export function $get(key) { | |
if (inReaction()) { | |
const administration = this.$mobx; | |
const atom = administration.getDataAtom(key); | |
atom.has.reportObserved(); | |
administration.reportObserved(); | |
if (!this.has(key)) { | |
return undefined; | |
} | |
atom.write.reportObserved(); | |
} | |
return this.get(key); | |
} | |
export function $clear() { | |
transaction(() => { | |
const target = this.reference; | |
for (let key of this.keys()) { | |
target.delete(key); | |
} | |
}); | |
} | |
export function $toString() { | |
const administration = this.$mobx; | |
administration.any.reportObserved(); | |
administration.reportObserved(); | |
// this doesn't necessarily need to be fast | |
return administration.name + '[{ ' + [...this.keys()].map(key => `${key}: ${'' + this.get(key)}`).join(', ') + ' }]'; | |
} | |
export function $toJS() { | |
const administration = this.$mobx; | |
administration.any.reportObserved(); | |
administration.reportObserved(); | |
return this.reference; | |
} | |
export function $toJSON() { | |
const administration = this.$mobx; | |
administration.any.reportObserved(); | |
administration.reportObserved(); | |
let obj = {}; | |
for (let entry of this) { | |
obj[entry[0]] = entry[1]; | |
} | |
return obj; | |
} | |
export function $replace(map) { | |
const target = this.reference; | |
if (map === null || map === undefined) { | |
target.clear(); | |
return; | |
} | |
if (Array.isArray(map) || isObservableArray(map)) { | |
map = new Map(map); | |
} | |
else if (isPlainObject(map) || isObservableObject(map)) { | |
map = new Map(Object.entries(map)); | |
} | |
if (map instanceof Map || isObservableMap(map)) { | |
if (map.size === 0) { | |
target.clear(); | |
return; | |
} | |
const store = this; | |
transaction(function() { | |
for (let key of store.keys()) { | |
if (!map.has(key)) { | |
target.delete(key); | |
} | |
} | |
target.merge(map); | |
}); | |
} | |
else { | |
throw new Error('Must replace map with another map, an object, or an array'); | |
} | |
return target; | |
} | |
export function $merge(other) { | |
if (other === undefined || other === null) { | |
return; | |
} | |
const target = this.reference; | |
transaction(function() { | |
if (Array.isArray(other) || isObservableArray(other)) { | |
other.forEach(function(value) { target.set(value[0], value[1]); }); | |
} | |
else if (other instanceof Map || isObservableMap(other)) { | |
other.forEach(function(value, key) { target.set(key, value); }); | |
} | |
else if (isPlainObject(other) || isObservableObject(other)) { | |
Object.entries(other).forEach(function(entry) { target.set(entry[0], entry[1]); }); | |
} | |
else { | |
throw new Error('Cannot initialize map from ' + other); | |
} | |
}); | |
return target; | |
} | |
export function $forEach(callback) { | |
const administration = this.$mobx; | |
administration.any.reportObserved(); | |
administration.reportObserved(); | |
return this.forEach(callback); | |
} | |
export function $entries(){ | |
const administration = this.$mobx; | |
administration.any.reportObserved(); | |
administration.reportObserved(); | |
return this.entries(); | |
} | |
export function $values() { | |
const administration = this.$mobx; | |
administration.any.reportObserved(); | |
administration.reportObserved(); | |
return this.values(); | |
} | |
export function $keys() { | |
const administration = this.$mobx; | |
administration.any.reportObserved(); | |
administration.reportObserved(); | |
return this.keys(); | |
} | |
export function $symbolIterator(){ | |
const administration = this.$mobx; | |
administration.any.reportObserved(); | |
administration.reportObserved(); | |
return this[Symbol.iterator](); | |
} | |
export function $observe(listener, fireImmediately){ | |
const disposer = registerListener(this.$mobx, listener); | |
if (fireImmediately) { | |
for (let entry of this) { | |
listener(createAddChange(this, entry[0], entry[1])); | |
} | |
} | |
return disposer; | |
} | |
export function $intercept(interceptor) { | |
return registerInterceptor(this.$mobx, interceptor); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment