simple-bound
Version:
A simple and customizable reactive data-binding library.
414 lines (399 loc) • 15.6 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/* global Reflect, Promise */
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
function __extends(d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
}
var BoundError = /** @class */ (function (_super) {
__extends(BoundError, _super);
function BoundError(message) {
/* istanbul ignore next */
return _super.call(this, message ? "[bound]: " + message : message) || this;
}
return BoundError;
}(Error));
var config = {
debug: false
};
/**
* Responsible for binding objects' properties together, storing their values inside and updating subscribers.
*
* It helps to manipulate bindings on the lowest possible level.
*
* It only binds a SINGLE property at a time!
*
* @template T captures a type of property to bind. Once the class is initialied - only properties of types that extend T are allowed for binding.
*/
var Binding = /** @class */ (function () {
/**
* Creates an instance of Binding.
* @param twoWay defines if a binding should always be 2-way and ignore roles.
* @param value initial value to assign to slave bindings.
* @param [plugins] to call on events.
*/
function Binding(twoWay, value, plugins) {
this.twoWay = twoWay;
this.value = value;
this.plugins = plugins;
/**
* Stores subscribers for further manipulations.
*/
this.subscribers = [];
}
/**
* Responsible for executing the plugins synchronyously,
*
* @param type describes the type of action to be transmitted to a plugin
*/
Binding.prototype.callPlugins = function (type) {
var _this = this;
if (this.plugins) {
this.plugins.forEach(function (plugin) { return plugin && plugin(_this.value, Object.freeze({
type: type,
subscribers: _this.subscribers
})); });
}
};
/**
* Adds a subscriber to the list of subscribers.
*
* @param subscriber to add
*/
Binding.prototype.bind = function (subscriber) {
if (this.subscribers.every(function (b) { return !Binding.subscriptionsEqual(b, subscriber); })) {
this.subscribers.push(subscriber);
}
else if (Binding.config.debug) {
throw new BoundError("Binding for " + subscriber.prop + " is already declared.");
}
return subscriber;
};
/**
* A generic get function that is applied to subscribers.
*
* Can also be used to get the current binding value.
*/
Binding.prototype.get = function () {
this.callPlugins('get');
return this.value;
};
/**
* A generic set function that is applied to subscribers.
*
* Can also be used to set the current binding value.
*/
Binding.prototype.set = function (newValue) {
// Bind value for all masters at once
this.value = newValue;
// Then notify all slaves about the change
this.notify(newValue);
// Then call plugins
this.callPlugins('set');
};
/**
* Asynchroniously notifies the subscribers about the value change.
*
* @param newValue is the value to set to subscribers' properties.
*/
Binding.prototype.notify = function (newValue) {
var _this = this;
return new Promise(function (resolve, _) {
_this.subscribers.forEach(function (subscriber) {
if (subscriber.role !== 'master') { // Set value for each slave
subscriber.obj[subscriber.prop] = newValue;
}
});
resolve();
});
};
Binding.prototype.addSubscriber = function (obj, prop, role) {
if (this.twoWay || role === 'master') {
if (obj[prop] !== undefined) {
// Bind value for all masters at once
this.value = obj[prop];
// Then notify all slaves about the change
this.notify(this.value);
}
else {
obj[prop] = this.value;
}
this.bind({ obj: obj, prop: prop, role: 'master' });
// TODO: account for a case of having enumerable get/set on a prop instead of normal value
Object.defineProperty(obj, prop, {
get: this.get.bind(this),
set: this.set.bind(this),
enumerable: true
});
}
else {
obj[prop] = this.value;
this.bind({ obj: obj, prop: prop, role: 'slave' });
}
return this;
};
Binding.prototype.addMasterSubscriber = function (obj, prop) {
return this.addSubscriber(obj, prop, 'master');
};
Binding.prototype.addSlaveSubscriber = function (obj, prop) {
return this.addSubscriber(obj, prop, 'slave');
};
Binding.prototype.removeSubscriber = function () {
var index = -1;
if (typeof arguments[0] === 'number') {
index = arguments[0];
}
else {
var obj_1 = arguments[0];
var prop_1 = arguments[1];
index = this.subscribers.findIndex(function (b) { return Binding.subscriptionsEqual(b, { obj: obj_1, prop: prop_1 }); });
}
if (index !== -1) {
// Also remove getters and setters
if (this.subscribers[index].role === 'master') {
Object.defineProperty(this.subscribers[index].obj, this.subscribers[index].prop, {
value: this.value,
writable: true
});
}
this.subscribers.splice(index, 1);
}
return this;
};
/**
* Clears all subscribers from the binding.
*/
Binding.prototype.clearSubscribers = function () {
var _this = this;
this.subscribers.forEach(function (_, index) { return _this.removeSubscriber(index); });
return this;
};
Object.defineProperty(Binding, "config", {
/**
* Global binding config. Changes affect all instances.
*/
get: function () { return config; },
enumerable: true,
configurable: true
});
/**
* Checks subscribers' objects for reference equality.
*/
Binding.subscriptionsEqual = function (src1, src2) { return !!src1 && !!src2 && src1.prop === src2.prop && src1.obj === src2.obj; };
return Binding;
}());
/**
* fromPath
* Returns a value from an object by a given path (usually string).
*
* @param obj an object to get a value from.
* @param path to get a value by.
* @returns a value from a given path. If a path is invalid - returns undefined.
*/
function fromPath(obj, path) {
if (!path)
return obj;
if (typeof path === 'number' || !~path.indexOf('.'))
return obj[path];
return path.split('.').reduce(function (o, i) { return (o === Object(o) ? o[i] : o); }, obj);
}
/**
* assignToPath
* Assigns a value to an object by a given path (usually string).
* If the path is invalid, silently creates the required path and assigns a value
*
* @param obj an object to get a value from.
* @param path to get a value by.
* @param value a value to assign.
*/
function assignToPath(obj, path, value) {
if (!path)
return obj;
var pathArr = (typeof path === 'string' && ~path.indexOf('.')) ? path.split('.') : [path];
var key = pathArr.pop();
var final = pathArr.length === 0 ?
obj : pathArr.reduce(function (o, i) {
if (o[i] === undefined)
o[i] = {};
return o[i];
}, obj);
final[key] = value;
}
var hasProxy = !!Proxy;
var BaseBound = /** @class */ (function () {
/**
* Creates an instance of BaseBound.
* @param proto used as an object prototype for the creation of boundObject and storage. Doesn't become bound itself.
* @param [plugins] to plug into the binding events.
*/
function BaseBound(proto, plugins) {
this.plugins = plugins;
/**
* Stores bindings in a structure that is identical to the binding-prototype-object.
*/
this.storage = {};
/**
* A bound object created from a constuctor's snapshot object.
*
* Contains an instance of the Bound class itself by the `__bound__` key.
*/
this.boundObject = { __bound__: this };
// Make __bound__ non-enumerable.
Object.defineProperty(this.boundObject, '__bound__', {
value: this,
writable: true
});
if (BaseBound.config.debug && typeof proto !== 'object') {
throw new BoundError('Only object binds are allowed. For property and pure value bindings use Binding from "bound/binding".');
}
if (BaseBound.config.debug && proto instanceof BaseBound || BaseBound.isBound(proto)) {
throw new BoundError('Cannot rebind a bound object.');
}
}
/**
* [NOT_IMPLEMENTED] Maps the object of a different shape to the original binding object
* @param obj target object to bind
* @param mapToOriginal a map for target object's keys relative to the original binding object type
* @param twoWay whether the binding should be two-way
*/
BaseBound.prototype.bindAndMap = function (obj, mapToOriginal, twoWay) {
throw new BoundError('Method not implemented.');
};
Object.defineProperty(BaseBound, "config", {
/**
* Global binding config. Changes affect all instances.
*/
get: function () { return config; },
enumerable: true,
configurable: true
});
/**
* Checks whether an object is already bound.
*
* @param obj an object ot check
*/
BaseBound.isBound = function (obj) {
return !!obj.__bound__ && (obj.__bound__ instanceof BaseBound);
};
return BaseBound;
}());
/**
* Allows multiple full-object bindings.
* Stores bindings and binds objects together, providing the highest possible abstraction level for bindings.
*
* @extends {BaseBound<T>}
* @template T captures a type of proto object for later usage in binding type inference
*/
var Bound = /** @class */ (function (_super) {
__extends(Bound, _super);
/**
* Creates an instance of Bound using a proto object.
* @param proto used as an object prototype for the creation of boundObject and storage. Doesn't become bound itself.
* @param [plugins] to plug into the binding events. Do not work yet.
*/ //TODO: Bound plugins!
function Bound(proto, plugins) {
var _this = _super.call(this, proto, plugins) || this;
_this.storage = {};
var original = JSON.parse(JSON.stringify(proto));
for (var key in original) {
if (typeof original[key] === 'object') { // If the value is object - then treat it like another bound target
var bound = new Bound(original[key], (plugins || {})[key]);
_this.boundObject[key] = bound.boundObject;
_this.storage[key] = bound.storage;
}
else {
var binding = new Binding(false, original[key], [(plugins || {})[key]]);
binding.addSubscriber(_this.boundObject, key);
_this.storage[key] = binding;
}
}
return _this;
}
/**
* Binds an object to all other current subscribers
*
* @template U used to capture the bound object type. Must extends original template type.
* @param obj to bind
* @param [twoWay] whether the binding should be two-way
*/ //TODO: rework this function. It's a mess.
Bound.prototype.bind = function (obj, twoWay) {
var _this = this;
if (twoWay === void 0) { twoWay = true; }
var __bind = function (_obj, _twoWay, path) {
if (_twoWay === void 0) { _twoWay = true; }
if (path === void 0) { path = ''; }
Object.defineProperty(_obj, '__bound__', {
value: fromPath(_this.boundObject, path).__bound__,
writable: true
});
for (var key in fromPath(_this.storage, path)) {
var nextPath = !path ? key : path + "." + key;
var nextStorage = fromPath(_this.storage, nextPath);
var nextValue = _obj[key];
if (nextStorage instanceof Binding) {
nextStorage.addSubscriber(_obj, key, _twoWay ? 'master' : 'slave');
}
else {
__bind(nextValue, _twoWay, nextPath);
}
}
};
__bind(obj, twoWay);
return this;
};
/**
* Unbinds an object and destroys all of its listeners
*
* @param obj reference of object to be unbound
*/
Bound.prototype.unbind = function (obj) {
var _this = this;
var __unbind = function (_obj, path) {
if (path === void 0) { path = ''; }
_obj.__bound__ = undefined;
for (var key in fromPath(_this.storage, path)) {
var nextPath = !path ? key : path + "." + key;
var nextStorage = fromPath(_this.storage, nextPath);
var nextValue = _obj[key];
if (nextStorage instanceof Binding) {
nextStorage.removeSubscriber(_obj, key);
}
else {
_obj[key] = __unbind(nextValue, nextPath);
}
}
return JSON.parse(JSON.stringify(_obj));
};
return __unbind(obj);
};
return Bound;
}(BaseBound));
// TODO: account for a class decorator case
function bound(target) {
return new Bound(target).boundObject;
}
exports.bound = bound;
exports.default = Bound;
exports.Binding = Binding;
exports.BoundBase = BaseBound;
exports.BoundError = BoundError;
exports.fromPath = fromPath;
exports.assignToPath = assignToPath;
exports.hasProxy = hasProxy;
//# sourceMappingURL=bound.cjs.js.map