mobx-utils
Version:
Utility functions and common patterns for MobX
1,237 lines (1,222 loc) • 69.7 kB
JavaScript
import { action, extendObservable, observable, _allowStateChanges, createAtom, computed, observe, makeObservable, runInAction, keys, isObservableArray, isObservableMap, isComputed, isObservableObject, $mobx, isComputedProp, _getAdministration, getAtom, isAction, autorun, _isComputingDerivation, onBecomeUnobserved, entries, values, transaction, reaction, ObservableMap, _getGlobalState } from 'mobx';
var NOOP = function () { };
var IDENTITY = function (_) { return _; };
function fail(message) {
throw new Error("[mobx-utils] " + message);
}
function invariant(cond, message) {
if (message === void 0) { message = "Illegal state"; }
if (!cond)
fail(message);
}
function addHiddenProp(object, propName, value) {
Object.defineProperty(object, propName, {
enumerable: false,
writable: true,
configurable: true,
value: value,
});
}
var deepFields = function (x) {
return (x &&
x !== Object.prototype &&
Object.getOwnPropertyNames(x).concat(deepFields(Object.getPrototypeOf(x)) || []));
};
var distinctDeepFields = function (x) {
var deepFieldsIndistinct = deepFields(x);
var deepFieldsDistinct = deepFieldsIndistinct.filter(function (item, index) { return deepFieldsIndistinct.indexOf(item) === index; });
return deepFieldsDistinct;
};
var getAllMethodsAndProperties = function (x) {
return distinctDeepFields(x).filter(function (name) { return name !== "constructor" && !~name.indexOf("__"); });
};
var PENDING = "pending";
var FULFILLED = "fulfilled";
var REJECTED = "rejected";
function caseImpl(handlers) {
switch (this.state) {
case PENDING:
return handlers.pending && handlers.pending(this.value);
case REJECTED:
return handlers.rejected && handlers.rejected(this.value);
case FULFILLED:
return handlers.fulfilled ? handlers.fulfilled(this.value) : this.value;
}
}
/**
* `fromPromise` takes a Promise, extends it with 2 observable properties that track
* the status of the promise and returns it. The returned object has the following observable properties:
* - `value`: either the initial value, the value the Promise resolved to, or the value the Promise was rejected with. use `.state` if you need to be able to tell the difference.
* - `state`: one of `"pending"`, `"fulfilled"` or `"rejected"`
*
* And the following methods:
* - `case({fulfilled, rejected, pending})`: maps over the result using the provided handlers, or returns `undefined` if a handler isn't available for the current promise state.
*
* The returned object implements `PromiseLike<TValue>`, so you can chain additional `Promise` handlers using `then`. You may also use it with `await` in `async` functions.
*
* Note that the status strings are available as constants:
* `mobxUtils.PENDING`, `mobxUtils.REJECTED`, `mobxUtil.FULFILLED`
*
* fromPromise takes an optional second argument, a previously created `fromPromise` based observable.
* This is useful to replace one promise based observable with another, without going back to an intermediate
* "pending" promise state while fetching data. For example:
*
* @example
* \@observer
* class SearchResults extends React.Component {
* \@observable.ref searchResults
*
* componentDidUpdate(nextProps) {
* if (nextProps.query !== this.props.query)
* this.searchResults = fromPromise(
* window.fetch("/search?q=" + nextProps.query),
* // by passing, we won't render a pending state if we had a successful search query before
* // rather, we will keep showing the previous search results, until the new promise resolves (or rejects)
* this.searchResults
* )
* }
*
* render() {
* return this.searchResults.case({
* pending: (staleValue) => {
* return staleValue || "searching" // <- value might set to previous results while the promise is still pending
* },
* fulfilled: (value) => {
* return value // the fresh results
* },
* rejected: (error) => {
* return "Oops: " + error
* }
* })
* }
* }
*
* Observable promises can be created immediately in a certain state using
* `fromPromise.reject(reason)` or `fromPromise.resolve(value?)`.
* The main advantage of `fromPromise.resolve(value)` over `fromPromise(Promise.resolve(value))` is that the first _synchronously_ starts in the desired state.
*
* It is possible to directly create a promise using a resolve, reject function:
* `fromPromise((resolve, reject) => setTimeout(() => resolve(true), 1000))`
*
* @example
* const fetchResult = fromPromise(fetch("http://someurl"))
*
* // combine with when..
* when(
* () => fetchResult.state !== "pending",
* () => {
* console.log("Got ", fetchResult.value)
* }
* )
*
* // or a mobx-react component..
* const myComponent = observer(({ fetchResult }) => {
* switch(fetchResult.state) {
* case "pending": return <div>Loading...</div>
* case "rejected": return <div>Ooops... {fetchResult.value}</div>
* case "fulfilled": return <div>Gotcha: {fetchResult.value}</div>
* }
* })
*
* // or using the case method instead of switch:
*
* const myComponent = observer(({ fetchResult }) =>
* fetchResult.case({
* pending: () => <div>Loading...</div>,
* rejected: error => <div>Ooops.. {error}</div>,
* fulfilled: value => <div>Gotcha: {value}</div>,
* }))
*
* // chain additional handler(s) to the resolve/reject:
*
* fetchResult.then(
* (result) => doSomeTransformation(result),
* (rejectReason) => console.error('fetchResult was rejected, reason: ' + rejectReason)
* ).then(
* (transformedResult) => console.log('transformed fetchResult: ' + transformedResult)
* )
*
* @param origPromise The promise which will be observed
* @param oldPromise The previously observed promise
* @returns origPromise with added properties and methods described above.
*/
function fromPromise(origPromise, oldPromise) {
invariant(arguments.length <= 2, "fromPromise expects up to two arguments");
invariant(typeof origPromise === "function" ||
(typeof origPromise === "object" &&
origPromise &&
typeof origPromise.then === "function"), "Please pass a promise or function to fromPromise");
if (origPromise.isPromiseBasedObservable === true)
return origPromise;
if (typeof origPromise === "function") {
// If it is a (reject, resolve function, wrap it)
origPromise = new Promise(origPromise);
}
var promise = origPromise;
origPromise.then(action("observableFromPromise-resolve", function (value) {
promise.value = value;
promise.state = FULFILLED;
}), action("observableFromPromise-reject", function (reason) {
promise.value = reason;
promise.state = REJECTED;
}));
promise.isPromiseBasedObservable = true;
promise.case = caseImpl;
var oldData = oldPromise && (oldPromise.state === FULFILLED || oldPromise.state === PENDING)
? oldPromise.value
: undefined;
extendObservable(promise, {
value: oldData,
state: PENDING,
}, {}, { deep: false });
return promise;
}
(function (fromPromise) {
fromPromise.reject = action("fromPromise.reject", function (reason) {
var p = fromPromise(Promise.reject(reason));
p.state = REJECTED;
p.value = reason;
return p;
});
function resolveBase(value) {
if (value === void 0) { value = undefined; }
var p = fromPromise(Promise.resolve(value));
p.state = FULFILLED;
p.value = value;
return p;
}
fromPromise.resolve = action("fromPromise.resolve", resolveBase);
})(fromPromise || (fromPromise = {}));
/**
* Returns true if the provided value is a promise-based observable.
* @param value any
* @returns {boolean}
*/
function isPromiseBasedObservable(value) {
return value && value.isPromiseBasedObservable === true;
}
var __spreadArrays = (undefined && undefined.__spreadArrays) || function () {
for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
for (var r = Array(s), k = 0, i = 0; i < il; i++)
for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
r[k] = a[j];
return r;
};
/**
* Moves an item from one position to another, checking that the indexes given are within bounds.
*
* @example
* const source = observable([1, 2, 3])
* moveItem(source, 0, 1)
* console.log(source.map(x => x)) // [2, 1, 3]
*
* @export
* @param {ObservableArray<T>} target
* @param {number} fromIndex
* @param {number} toIndex
* @returns {ObservableArray<T>}
*/
function moveItem(target, fromIndex, toIndex) {
checkIndex(target, fromIndex);
checkIndex(target, toIndex);
if (fromIndex === toIndex) {
return;
}
var oldItems = target.slice();
var newItems;
if (fromIndex < toIndex) {
newItems = __spreadArrays(oldItems.slice(0, fromIndex), oldItems.slice(fromIndex + 1, toIndex + 1), [
oldItems[fromIndex]
], oldItems.slice(toIndex + 1));
}
else {
// toIndex < fromIndex
newItems = __spreadArrays(oldItems.slice(0, toIndex), [
oldItems[fromIndex]
], oldItems.slice(toIndex, fromIndex), oldItems.slice(fromIndex + 1));
}
target.replace(newItems);
return target;
}
/**
* Checks whether the specified index is within bounds. Throws if not.
*
* @private
* @param {ObservableArray<any>} target
* @param {number }index
*/
function checkIndex(target, index) {
if (index < 0) {
throw new Error("[mobx.array] Index out of bounds: " + index + " is negative");
}
var length = target.length;
if (index >= length) {
throw new Error("[mobx.array] Index out of bounds: " + index + " is not smaller than " + length);
}
}
/**
* `lazyObservable` creates an observable around a `fetch` method that will not be invoked
* until the observable is needed the first time.
* The fetch method receives a `sink` callback which can be used to replace the
* current value of the lazyObservable. It is allowed to call `sink` multiple times
* to keep the lazyObservable up to date with some external resource.
*
* Note that it is the `current()` call itself which is being tracked by MobX,
* so make sure that you don't dereference to early.
*
* @example
* const userProfile = lazyObservable(
* sink => fetch("/myprofile").then(profile => sink(profile))
* )
*
* // use the userProfile in a React component:
* const Profile = observer(({ userProfile }) =>
* userProfile.current() === undefined
* ? <div>Loading user profile...</div>
* : <div>{userProfile.current().displayName}</div>
* )
*
* // triggers refresh the userProfile
* userProfile.refresh()
*
* @param {(sink: (newValue: T) => void) => void} fetch method that will be called the first time the value of this observable is accessed. The provided sink can be used to produce a new value, synchronously or asynchronously
* @param {T} [initialValue=undefined] optional initialValue that will be returned from `current` as long as the `sink` has not been called at least once
* @returns {{
* current(): T,
* refresh(): T,
* reset(): T
* pending: boolean
* }}
*/
function lazyObservable(fetch, initialValue) {
if (initialValue === void 0) { initialValue = undefined; }
var started = false;
var value = observable.box(initialValue, { deep: false });
var pending = observable.box(false);
var currentFnc = function () {
if (!started) {
started = true;
_allowStateChanges(true, function () {
pending.set(true);
});
fetch(function (newValue) {
_allowStateChanges(true, function () {
value.set(newValue);
pending.set(false);
});
});
}
return value.get();
};
var resetFnc = action("lazyObservable-reset", function () {
started = false;
value.set(initialValue);
return value.get();
});
return {
current: currentFnc,
refresh: function () {
if (started) {
started = false;
return currentFnc();
}
else {
return value.get();
}
},
reset: function () {
return resetFnc();
},
get pending() {
return pending.get();
},
};
}
/**
* `fromResource` creates an observable whose current state can be inspected using `.current()`,
* and which can be kept in sync with some external datasource that can be subscribed to.
*
* The created observable will only subscribe to the datasource if it is in use somewhere,
* (un)subscribing when needed. To enable `fromResource` to do that two callbacks need to be provided,
* one to subscribe, and one to unsubscribe. The subscribe callback itself will receive a `sink` callback, which can be used
* to update the current state of the observable, allowing observes to react.
*
* Whatever is passed to `sink` will be returned by `current()`. The values passed to the sink will not be converted to
* observables automatically, but feel free to do so.
* It is the `current()` call itself which is being tracked,
* so make sure that you don't dereference to early.
*
* For inspiration, an example integration with the apollo-client on [github](https://github.com/apollostack/apollo-client/issues/503#issuecomment-241101379),
* or the [implementation](https://github.com/mobxjs/mobx-utils/blob/1d17cf7f7f5200937f68cc0b5e7ec7f3f71dccba/src/now.ts#L43-L57) of `mobxUtils.now`
*
* The following example code creates an observable that connects to a `dbUserRecord`,
* which comes from an imaginary database and notifies when it has changed.
*
* @example
* function createObservableUser(dbUserRecord) {
* let currentSubscription;
* return fromResource(
* (sink) => {
* // sink the current state
* sink(dbUserRecord.fields)
* // subscribe to the record, invoke the sink callback whenever new data arrives
* currentSubscription = dbUserRecord.onUpdated(() => {
* sink(dbUserRecord.fields)
* })
* },
* () => {
* // the user observable is not in use at the moment, unsubscribe (for now)
* dbUserRecord.unsubscribe(currentSubscription)
* }
* )
* }
*
* // usage:
* const myUserObservable = createObservableUser(myDatabaseConnector.query("name = 'Michel'"))
*
* // use the observable in autorun
* autorun(() => {
* // printed everytime the database updates its records
* console.log(myUserObservable.current().displayName)
* })
*
* // ... or a component
* const userComponent = observer(({ user }) =>
* <div>{user.current().displayName}</div>
* )
*
* @export
* @template T
* @param {(sink: (newValue: T) => void) => void} subscriber
* @param {IDisposer} [unsubscriber=NOOP]
* @param {T} [initialValue=undefined] the data that will be returned by `get()` until the `sink` has emitted its first data
* @returns {{
* current(): T;
* dispose(): void;
* isAlive(): boolean;
* }}
*/
function fromResource(subscriber, unsubscriber, initialValue) {
if (unsubscriber === void 0) { unsubscriber = NOOP; }
if (initialValue === void 0) { initialValue = undefined; }
var isActive = false;
var isDisposed = false;
var value = initialValue;
var suspender = function () {
if (isActive) {
isActive = false;
unsubscriber();
}
};
var atom = createAtom("ResourceBasedObservable", function () {
invariant(!isActive && !isDisposed);
isActive = true;
subscriber(function (newValue) {
_allowStateChanges(true, function () {
value = newValue;
atom.reportChanged();
});
});
}, suspender);
return {
current: function () {
invariant(!isDisposed, "subscribingObservable has already been disposed");
var isBeingTracked = atom.reportObserved();
if (!isBeingTracked && !isActive)
console.warn("Called `get` of a subscribingObservable outside a reaction. Current value will be returned but no new subscription has started");
return value;
},
dispose: function () {
isDisposed = true;
suspender();
},
isAlive: function () { return isActive; },
};
}
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function observableSymbol() {
return (typeof Symbol === "function" && Symbol.observable) || "@@observable";
}
/**
* Converts an expression to an observable stream (a.k.a. TC 39 Observable / RxJS observable).
* The provided expression is tracked by mobx as long as there are subscribers, automatically
* emitting when new values become available. The expressions respect (trans)actions.
*
* @example
*
* const user = observable({
* firstName: "C.S",
* lastName: "Lewis"
* })
*
* Rx.Observable
* .from(mobxUtils.toStream(() => user.firstname + user.lastName))
* .scan(nameChanges => nameChanges + 1, 0)
* .subscribe(nameChanges => console.log("Changed name ", nameChanges, "times"))
*
* @export
* @template T
* @param {() => T} expression
* @param {boolean} fireImmediately (by default false)
* @returns {IObservableStream<T>}
*/
function toStream(expression, fireImmediately) {
var _a;
if (fireImmediately === void 0) { fireImmediately = false; }
var computedValue = computed(expression);
return _a = {
subscribe: function (observer) {
if ("function" === typeof observer) {
return {
unsubscribe: observe(computedValue, function (_a) {
var newValue = _a.newValue;
return observer(newValue);
}, fireImmediately),
};
}
if (observer && "object" === typeof observer && observer.next) {
return {
unsubscribe: observe(computedValue, function (_a) {
var newValue = _a.newValue;
return observer.next(newValue);
}, fireImmediately),
};
}
return {
unsubscribe: function () { },
};
}
},
_a[observableSymbol()] = function () {
return this;
},
_a;
}
var StreamListener = /** @class */ (function () {
function StreamListener(observable, initialValue) {
var _this = this;
Object.defineProperty(this, "current", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "subscription", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
makeObservable(this);
runInAction(function () {
_this.current = initialValue;
_this.subscription = observable.subscribe(_this);
});
}
Object.defineProperty(StreamListener.prototype, "dispose", {
enumerable: false,
configurable: true,
writable: true,
value: function () {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
});
Object.defineProperty(StreamListener.prototype, "next", {
enumerable: false,
configurable: true,
writable: true,
value: function (value) {
this.current = value;
}
});
Object.defineProperty(StreamListener.prototype, "complete", {
enumerable: false,
configurable: true,
writable: true,
value: function () {
this.dispose();
}
});
Object.defineProperty(StreamListener.prototype, "error", {
enumerable: false,
configurable: true,
writable: true,
value: function (value) {
this.current = value;
this.dispose();
}
});
__decorate([
observable.ref
], StreamListener.prototype, "current", void 0);
__decorate([
action.bound
], StreamListener.prototype, "next", null);
__decorate([
action.bound
], StreamListener.prototype, "complete", null);
__decorate([
action.bound
], StreamListener.prototype, "error", null);
return StreamListener;
}());
function fromStream(observable, initialValue) {
if (initialValue === void 0) { initialValue = undefined; }
return new StreamListener(observable, initialValue);
}
var __assign = (undefined && undefined.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __decorate$1 = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var RESERVED_NAMES = ["model", "reset", "submit", "isDirty", "isPropertyDirty", "resetProperty"];
var ViewModel = /** @class */ (function () {
function ViewModel(model) {
var _this = this;
Object.defineProperty(this, "model", {
enumerable: true,
configurable: true,
writable: true,
value: model
});
Object.defineProperty(this, "localValues", {
enumerable: true,
configurable: true,
writable: true,
value: observable.map({})
});
Object.defineProperty(this, "localComputedValues", {
enumerable: true,
configurable: true,
writable: true,
value: observable.map({})
});
Object.defineProperty(this, "isPropertyDirty", {
enumerable: true,
configurable: true,
writable: true,
value: function (key) {
return _this.localValues.has(key);
}
});
makeObservable(this);
invariant(isObservableObject(model), "createViewModel expects an observable object");
var ownMethodsAndProperties = getAllMethodsAndProperties(this);
// use this helper as Object.getOwnPropertyNames doesn't return getters
getAllMethodsAndProperties(model).forEach(function (key) {
var _a;
if (ownMethodsAndProperties.includes(key)) {
return;
}
if (key === $mobx || key === "__mobxDidRunLazyInitializers") {
return;
}
invariant(RESERVED_NAMES.indexOf(key) === -1, "The propertyname " + key + " is reserved and cannot be used with viewModels");
if (isComputedProp(model, key)) {
var computedBox = _getAdministration(model, key); // Fixme: there is no clear api to get the derivation
var get = computedBox.derivation.bind(_this);
var set = (_a = computedBox.setter_) === null || _a === void 0 ? void 0 : _a.bind(_this);
_this.localComputedValues.set(key, computed(get, { set: set }));
}
var descriptor = Object.getOwnPropertyDescriptor(model, key);
var additionalDescriptor = descriptor ? { enumerable: descriptor.enumerable } : {};
Object.defineProperty(_this, key, __assign(__assign({}, additionalDescriptor), { configurable: true, get: function () {
if (isComputedProp(model, key))
return _this.localComputedValues.get(key).get();
if (_this.isPropertyDirty(key))
return _this.localValues.get(key);
else
return _this.model[key];
}, set: action(function (value) {
if (isComputedProp(model, key)) {
_this.localComputedValues.get(key).set(value);
}
else if (value !== _this.model[key]) {
_this.localValues.set(key, value);
}
else {
_this.localValues.delete(key);
}
}) }));
});
}
Object.defineProperty(ViewModel.prototype, "isDirty", {
get: function () {
return this.localValues.size > 0;
},
enumerable: false,
configurable: true
});
Object.defineProperty(ViewModel.prototype, "changedValues", {
get: function () {
return new Map(this.localValues);
},
enumerable: false,
configurable: true
});
Object.defineProperty(ViewModel.prototype, "submit", {
enumerable: false,
configurable: true,
writable: true,
value: function () {
var _this = this;
keys(this.localValues).forEach(function (key) {
var source = _this.localValues.get(key);
var destination = _this.model[key];
if (isObservableArray(destination)) {
destination.replace(source);
}
else if (isObservableMap(destination)) {
destination.clear();
destination.merge(source);
}
else if (!isComputed(source)) {
_this.model[key] = source;
}
});
this.localValues.clear();
}
});
Object.defineProperty(ViewModel.prototype, "reset", {
enumerable: false,
configurable: true,
writable: true,
value: function () {
this.localValues.clear();
}
});
Object.defineProperty(ViewModel.prototype, "resetProperty", {
enumerable: false,
configurable: true,
writable: true,
value: function (key) {
this.localValues.delete(key);
}
});
__decorate$1([
computed
], ViewModel.prototype, "isDirty", null);
__decorate$1([
computed
], ViewModel.prototype, "changedValues", null);
__decorate$1([
action.bound
], ViewModel.prototype, "submit", null);
__decorate$1([
action.bound
], ViewModel.prototype, "reset", null);
__decorate$1([
action.bound
], ViewModel.prototype, "resetProperty", null);
return ViewModel;
}());
/**
* `createViewModel` takes an object with observable properties (model)
* and wraps a viewmodel around it. The viewmodel proxies all enumerable properties of the original model with the following behavior:
* - as long as no new value has been assigned to the viewmodel property, the original property will be returned.
* - any future change in the model will be visible in the viewmodel as well unless the viewmodel property was dirty at the time of the attempted change.
* - once a new value has been assigned to a property of the viewmodel, that value will be returned during a read of that property in the future. However, the original model remain untouched until `submit()` is called.
*
* The viewmodel exposes the following additional methods, besides all the enumerable properties of the model:
* - `submit()`: copies all the values of the viewmodel to the model and resets the state
* - `reset()`: resets the state of the viewmodel, abandoning all local modifications
* - `resetProperty(propName)`: resets the specified property of the viewmodel
* - `isDirty`: observable property indicating if the viewModel contains any modifications
* - `isPropertyDirty(propName)`: returns true if the specified property is dirty
* - `changedValues`: returns a key / value map with the properties that have been changed in the model so far
* - `model`: The original model object for which this viewModel was created
*
* You may use observable arrays, maps and objects with `createViewModel` but keep in mind to assign fresh instances of those to the viewmodel's properties, otherwise you would end up modifying the properties of the original model.
* Note that if you read a non-dirty property, viewmodel only proxies the read to the model. You therefore need to assign a fresh instance not only the first time you make the assignment but also after calling `reset()` or `submit()`.
*
* @example
* class Todo {
* \@observable title = "Test"
* }
*
* const model = new Todo()
* const viewModel = createViewModel(model);
*
* autorun(() => console.log(viewModel.model.title, ",", viewModel.title))
* // prints "Test, Test"
* model.title = "Get coffee"
* // prints "Get coffee, Get coffee", viewModel just proxies to model
* viewModel.title = "Get tea"
* // prints "Get coffee, Get tea", viewModel's title is now dirty, and the local value will be printed
* viewModel.submit()
* // prints "Get tea, Get tea", changes submitted from the viewModel to the model, viewModel is proxying again
* viewModel.title = "Get cookie"
* // prints "Get tea, Get cookie" // viewModel has diverged again
* viewModel.reset()
* // prints "Get tea, Get tea", changes of the viewModel have been abandoned
*
* @param {T} model
* @returns {(T & IViewModel<T>)}
* ```
*/
function createViewModel(model) {
return new ViewModel(model);
}
/**
* MobX normally suspends any computed value that is not in use by any reaction,
* and lazily re-evaluates the expression if needed outside a reaction while not in use.
* `keepAlive` marks a computed value as always in use, meaning that it will always fresh, but never disposed automatically.
*
* @example
* const obj = observable({
* number: 3,
* doubler: function() { return this.number * 2 }
* })
* const stop = keepAlive(obj, "doubler")
*
* @param {Object} target an object that has a computed property, created by `@computed` or `extendObservable`
* @param {string} property the name of the property to keep alive
* @returns {IDisposer} stops this keep alive so that the computed value goes back to normal behavior
*/
/**
* @example
* const number = observable(3)
* const doubler = computed(() => number.get() * 2)
* const stop = keepAlive(doubler)
* // doubler will now stay in sync reactively even when there are no further observers
* stop()
* // normal behavior, doubler results will be recomputed if not observed but needed, but lazily
*
* @param {IComputedValue<any>} computedValue created using the `computed` function
* @returns {IDisposer} stops this keep alive so that the computed value goes back to normal behavior
*/
function keepAlive(_1, _2) {
var computed = getAtom(_1, _2);
if (!computed)
throw new Error("No computed provided, please provide an object created with `computed(() => expr)` or an object + property name");
return observe(computed, function () { });
}
/**
* `queueProcessor` takes an observable array, observes it and calls `processor`
* once for each item added to the observable array, optionally debouncing the action
*
* @example
* const pendingNotifications = observable([])
* const stop = queueProcessor(pendingNotifications, msg => {
* // show Desktop notification
* new Notification(msg);
* })
*
* // usage:
* pendingNotifications.push("test!")
*
* @param {T[]} observableArray observable array instance to track
* @param {(item: T) => void} processor action to call per item
* @param {number} [debounce=0] optional debounce time in ms. With debounce 0 the processor will run synchronously
* @returns {IDisposer} stops the processor
*/
function queueProcessor(observableArray, processor, debounce) {
if (debounce === void 0) { debounce = 0; }
if (!isObservableArray(observableArray))
throw new Error("Expected observable array as first argument");
if (!isAction(processor))
processor = action("queueProcessor", processor);
var runner = function () {
// construct a final set
var items = observableArray.slice(0);
// clear the queue for next iteration
runInAction(function () { return observableArray.splice(0); });
// fire processor
items.forEach(processor);
};
if (debounce > 0)
return autorun(runner, { delay: debounce });
else
return autorun(runner);
}
/**
* `chunkProcessor` takes an observable array, observes it and calls `processor`
* once for a chunk of items added to the observable array, optionally deboucing the action.
* The maximum chunk size can be limited by number.
* This allows both, splitting larger into smaller chunks or (when debounced) combining smaller
* chunks and/or single items into reasonable chunks of work.
*
* @example
* const trackedActions = observable([])
* const stop = chunkProcessor(trackedActions, chunkOfMax10Items => {
* sendTrackedActionsToServer(chunkOfMax10Items);
* }, 100, 10)
*
* // usage:
* trackedActions.push("scrolled")
* trackedActions.push("hoveredButton")
* // when both pushes happen within 100ms, there will be only one call to server
*
* @param {T[]} observableArray observable array instance to track
* @param {(item: T[]) => void} processor action to call per item
* @param {number} [debounce=0] optional debounce time in ms. With debounce 0 the processor will run synchronously
* @param {number} [maxChunkSize=0] optionally do not call on full array but smaller chunks. With 0 it will process the full array.
* @returns {IDisposer} stops the processor
*/
function chunkProcessor(observableArray, processor, debounce, maxChunkSize) {
if (debounce === void 0) { debounce = 0; }
if (maxChunkSize === void 0) { maxChunkSize = 0; }
if (!isObservableArray(observableArray))
throw new Error("Expected observable array as first argument");
if (!isAction(processor))
processor = action("chunkProcessor", processor);
var runner = function () {
var _loop_1 = function () {
var chunkSize = maxChunkSize === 0
? observableArray.length
: Math.min(observableArray.length, maxChunkSize);
// construct a final set
var items = observableArray.slice(0, chunkSize);
// clear the slice for next iteration
runInAction(function () { return observableArray.splice(0, chunkSize); });
// fire processor
processor(items);
};
while (observableArray.length > 0) {
_loop_1();
}
};
if (debounce > 0)
return autorun(runner, { delay: debounce });
else
return autorun(runner);
}
var tickers = {};
/**
* Disposes of all the internal Observables created by invocations of `now()`.
*
* The use case for this is to ensure that unit tests can run independent of each other.
* You should not call this in regular application code.
*
* @example
* afterEach(() => {
* utils.resetNowInternalState()
* })
*/
function resetNowInternalState() {
for (var _i = 0, _a = Object.getOwnPropertyNames(tickers); _i < _a.length; _i++) {
var key = _a[_i];
tickers[key].dispose();
delete tickers[key];
}
}
/**
* Returns the current date time as epoch number.
* The date time is read from an observable which is updated automatically after the given interval.
* So basically it treats time as an observable.
*
* The function takes an interval as parameter, which indicates how often `now()` will return a new value.
* If no interval is given, it will update each second. If "frame" is specified, it will update each time a
* `requestAnimationFrame` is available.
*
* Multiple clocks with the same interval will automatically be synchronized.
*
* Countdown example: https://jsfiddle.net/mweststrate/na0qdmkw/
*
* @example
*
* const start = Date.now()
*
* autorun(() => {
* console.log("Seconds elapsed: ", (mobxUtils.now() - start) / 1000)
* })
*
*
* @export
* @param {(number | "frame")} [interval=1000] interval in milliseconds about how often the interval should update
* @returns
*/
function now(interval) {
if (interval === void 0) { interval = 1000; }
if (!_isComputingDerivation()) {
// See #40
return Date.now();
}
if (!tickers[interval]) {
if (typeof interval === "number")
tickers[interval] = createIntervalTicker(interval);
else
tickers[interval] = createAnimationFrameTicker();
}
return tickers[interval].current();
}
function createIntervalTicker(interval) {
var subscriptionHandle;
return fromResource(function (sink) {
sink(Date.now());
subscriptionHandle = setInterval(function () { return sink(Date.now()); }, interval);
}, function () {
clearInterval(subscriptionHandle);
}, Date.now());
}
function createAnimationFrameTicker() {
var frameBasedTicker = fromResource(function (sink) {
sink(Date.now());
function scheduleTick() {
window.requestAnimationFrame(function () {
sink(Date.now());
if (frameBasedTicker.isAlive())
scheduleTick();
});
}
scheduleTick();
}, function () { }, Date.now());
return frameBasedTicker;
}
/**
*`expr` can be used to create temporary computed values inside computed values.
* Nesting computed values is useful to create cheap computations in order to prevent expensive computations from needing to run.
* In the following example the expression prevents that a component is rerender _each time_ the selection changes;
* instead it will only rerenders when the current todo is (de)selected.
*
* `expr(func)` is an alias for `computed(func).get()`.
* Please note that the function given to `expr` is evaluated _twice_ in the scenario that the overall expression value changes.
* It is evaluated the first time when any observables it depends on change.
* It is evaluated a second time when a change in its value triggers the outer computed or reaction to evaluate, which recreates and reevaluates the expression.
*
* In the following example, the expression prevents the `TodoView` component from being re-rendered if the selection changes elsewhere.
* Instead, the component will only re-render when the relevant todo is (de)selected, which happens much less frequently.
*
* @example
* const Todo = observer((props) => {
* const todo = props.todo
* const isSelected = mobxUtils.expr(() => props.viewState.selection === todo)
* const TodoView = observer(({ todo, editorState }) => {
* const isSelected = mobxUtils.expr(() => editorState.selection === todo)
* return <div className={isSelected ? "todo todo-selected" : "todo"}>{todo.title}</div>
* })
*/
function expr(expr) {
if (!_isComputingDerivation())
console.warn("'expr' should only be used inside other reactive functions.");
// optimization: would be more efficient if the expr itself wouldn't be evaluated first on the next change, but just a 'changed' signal would be fired
return computed(expr).get();
}
var __assign$1 = (undefined && undefined.__assign) || function () {
__assign$1 = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign$1.apply(this, arguments);
};
var memoizationId = 0;
/**
* Creates a function that maps an object to a view.
* The mapping is memoized.
*
* See the [transformer](#createtransformer-in-detail) section for more details.
*
* @param transformer
* @param onCleanup
*/
function createTransformer(transformer, arg2) {
invariant(typeof transformer === "function" && transformer.length < 2, "createTransformer expects a function that accepts one argument");
// Memoizes: object id -> reactive view that applies transformer to the object
var views = {};
var onCleanup = undefined;
var keepAlive = false;
var debugNameGenerator = undefined;
if (typeof arg2 === "object") {
onCleanup = arg2.onCleanup;
keepAlive = arg2.keepAlive !== undefined ? arg2.keepAlive : false;
debugNameGenerator = arg2.debugNameGenerator;
}
else if (typeof arg2 === "function") {
onCleanup = arg2;
}
function createView(sourceIdentifier, sourceObject) {
var latestValue;
var computedValueOptions = {};
if (typeof arg2 === "object") {
onCleanup = arg2.onCleanup;
debugNameGenerator = arg2.debugNameGenerator;
computedValueOptions = arg2;
}
else if (typeof arg2 === "function") {
onCleanup = arg2;
}
else {
onCleanup = undefined;
debugNameGenerator = undefined;
}
var prettifiedName = debugNameGenerator
? debugNameGenerator(sourceObject)
: "Transformer-" + transformer.name + "-" + sourceIdentifier;
var expr = computed(function () {
return (latestValue = transformer(sourceObject));
}, __assign$1(__assign$1({}, computedValueOptions), { name: prettifiedName }));
if (!keepAlive) {
var disposer_1 = onBecomeUnobserved(expr, function () {
delete views[sourceIdentifier];
disposer_1();
if (onCleanup)
onCleanup(latestValue, sourceObject);
});
}
return expr;
}
var memoWarned = false;
return function (object) {
var identifier = getMemoizationId(object);
var reactiveView = views[identifier];
if (reactiveView)
return reactiveView.get();
if (!keepAlive && !_isComputingDerivation()) {
if (!memoWarned) {
console.warn("invoking a transformer from outside a reactive context won't memorized " +
"and is cleaned up immediately, unless keepAlive is set");
memoWarned = true;
}
var value = transformer(object);
if (onCleanup)
onCleanup(value, object);
return value;
}
// Not in cache; create a reactive view
reactiveView = views[identifier] = createView(identifier, object);
return reactiveView.get();
};
}
function getMemoizationId(object) {
var objectType = typeof object;
if (objectType === "string")
return "string:" + object;
if (objectType === "number")
return "number:" + object;
if (object === null || (objectType !== "object" && objectType !== "function"))
throw new Error("[mobx-utils] transform expected an object, function, string or number, got: " + String(object));
var tid = object.$transformId;
if (tid === undefined) {
tid = "memoizationId:" + ++memoizationId;
addHiddenProp(object, "$transformId", tid);
}
return tid;
}
function buildPath(entry) {
if (!entry)
return "ROOT";
var res = [];
while (entry.parent) {
res.push(entry.path);
entry = entry.parent;
}
return res.reverse().join("/");
}
function isRecursivelyObservable(thing) {
return isObservableObject(thing) || isObservableArray(thing) || isObservableMap(thing);
}
/**
* Given an object, deeply observes the given object.
* It is like `observe` from mobx, but applied recursively, including all future children.
*
* Note that the given object cannot ever contain cycles and should be a tree.
*
* As benefit: path and root will be provided in the callback, so the signature of the listener is
* (change, path, root) => void
*
* The returned disposer can be invoked to clean up the listener
*
* deepObserve cannot be used on computed values.
*
* @example
* const disposer = deepObserve(target, (change, path) => {
* console.dir(change)
* })
*/
function deepObserve(target, listener) {
var entrySet = new WeakMap();
function genericListener(change) {
var entry = entrySet.get(change.object);
processChange(change, entry);
listener(change, buildPath(entry), target);
}
function processChange(change, parent) {
switch (change.type) {
// Object changes
case "add": // also for map
observeRecursively(change.newValue, parent, change.name);
break;
case "update": // also for array and map
unobserveRecursively(change.oldValue);
observeRecursively(change.newValue, parent, change.name || "" + change.index);
break;
case "remove": // object
case "delete": // map
unobserveRecursively(change.oldValue);
break;
// Array changes
case "splice":
change.removed.map(unobserveRecursively);
change.added.forEach(function (value, idx) {
return observeRecursively(value, parent, "" + (change.index + idx));
});
// update paths
for (var i = change.index + change.addedCount; i < change.object.length; i++) {
if (isRecursivelyObservable(change.object[i])) {
var entry = entrySet.get(change.object[i]);
if (entry)
entry.path = "" + i;
}
}
break;
}
}
function observeRecursively(thing, parent, path) {
if (isRecursivelyObservable(thing)) {
var entry = entrySet.get(thing);
if (entry) {
if (entry.parent !== parent || entry.path !== path)
// MWE: this constraint is artificial, and this tool could be made to work with cycles,
// but it increases administration complexity, has tricky edge cases and the meaning of 'path'
// would become less clear. So doesn't seem to be needed for now
throw new Error("The same observable object cannot appear twice in the same tree," +
(" trying to assign it to '" + buildPath(parent) + "/" + path + "',") +
(" but it already exists at '" + buildPath(entry.parent) + "/" + entry.path + "'"));
}
else {
var entry_1 = {
parent: parent,
path: path,
dispose: observe(thing, genericListener),
};
entrySet.set(thing, entry_1);
entries(thing).forEach(function (_a) {
var key = _a[0], value = _a[1];
return observeRecursively(value, entry_1, key);
});
}
}
}
function unobserveRecursively(thing) {
if (isRecursivelyObservable(thing)) {
var entry = entrySet.get(thing);
if (!entry)
return;
entrySet.delete(thing);
entry.dispose();
values(thing).forEach(unobserveRecursively);
}
}