mobx-utils
Version:
Utility functions and common patterns for MobX
209 lines (208 loc) • 9.72 kB
JavaScript
var __assign = (this && this.__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 = (this && this.__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;
};
import { action, observable, isObservableObject, isObservableArray, isObservableMap, isComputedProp, isComputed, computed, keys, _getAdministration, $mobx, makeObservable, } from "mobx";
import { invariant, getAllMethodsAndProperties } from "./utils";
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([
computed
], ViewModel.prototype, "isDirty", null);
__decorate([
computed
], ViewModel.prototype, "changedValues", null);
__decorate([
action.bound
], ViewModel.prototype, "submit", null);
__decorate([
action.bound
], ViewModel.prototype, "reset", null);
__decorate([
action.bound
], ViewModel.prototype, "resetProperty", null);
return ViewModel;
}());
export { 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>)}
* ```
*/
export function createViewModel(model) {
return new ViewModel(model);
}