simple-bound
Version:
A simple and customizable reactive data-binding library.
164 lines • 5.63 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const boundError_1 = require("./boundError");
const config_1 = require("./config");
/**
* 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.
*/
class Binding {
/**
* 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.
*/
constructor(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
*/
callPlugins(type) {
if (this.plugins) {
this.plugins.forEach(plugin => plugin && plugin(this.value, Object.freeze({
type,
subscribers: this.subscribers
})));
}
}
/**
* Adds a subscriber to the list of subscribers.
*
* @param subscriber to add
*/
bind(subscriber) {
if (this.subscribers.every(b => !Binding.subscriptionsEqual(b, subscriber))) {
this.subscribers.push(subscriber);
}
else if (Binding.config.debug) {
throw new boundError_1.default(`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.
*/
get() {
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.
*/
set(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.
*/
notify(newValue) {
return new Promise((resolve, _) => {
this.subscribers.forEach(subscriber => {
if (subscriber.role !== 'master') { // Set value for each slave
subscriber.obj[subscriber.prop] = newValue;
}
});
resolve();
});
}
addSubscriber(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, 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, prop, role: 'slave' });
}
return this;
}
addMasterSubscriber(obj, prop) {
return this.addSubscriber(obj, prop, 'master');
}
addSlaveSubscriber(obj, prop) {
return this.addSubscriber(obj, prop, 'slave');
}
removeSubscriber() {
let index = -1;
if (typeof arguments[0] === 'number') {
index = arguments[0];
}
else {
const obj = arguments[0];
const prop = arguments[1];
index = this.subscribers.findIndex(b => Binding.subscriptionsEqual(b, { obj, prop }));
}
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.
*/
clearSubscribers() {
this.subscribers.forEach((_, index) => this.removeSubscriber(index));
return this;
}
/**
* Global binding config. Changes affect all instances.
*/
static get config() { return config_1.default; }
}
/**
* Checks subscribers' objects for reference equality.
*/
Binding.subscriptionsEqual = (src1, src2) => !!src1 && !!src2 && src1.prop === src2.prop && src1.obj === src2.obj;
exports.default = Binding;
//# sourceMappingURL=binding.js.map