Skip to content

Instantly share code, notes, and snippets.

@nestharus
Last active March 10, 2018 04:25
Show Gist options
  • Save nestharus/13b4d74f2ef4a2f4357dbd3fc23c1e54 to your computer and use it in GitHub Desktop.
Save nestharus/13b4d74f2ef4a2f4357dbd3fc23c1e54 to your computer and use it in GitHub Desktop.
mobx observable map
export function once(func) {
let invoked = false;
return function() {
if (invoked) {
return;
}
invoked = true;
return func();
};
}
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;
}
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';
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);
}
}
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();
});
});
export function isModifierDescriptor(thing) {
return thing !== null && typeof thing === 'object' && thing.isMobxModifierDescriptor === true;
}
export function isPlainObject(value) {
if (value === undefined || typeof value !== 'object') {
return false;
}
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}
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);
}
}
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();
});
});
import { extras } from 'mobx';
export function inReaction() {
return extras.getGlobalState().isRunningReactions;
}
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);
}
}
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;
}
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;
}
export * from './types-map-index';
export * from './types-administration';
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;
export * from './types-map-observable-map';
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];
}
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();
});
});
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