UNPKG

simple-bound

Version:

A simple and customizable reactive data-binding library.

417 lines (401 loc) 17.2 kB
var bound = (function (exports) { 'use strict'; /*! ***************************************************************************** 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; return exports; }({})); //# sourceMappingURL=bound.iife.js.map