props-model
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -1,1 +0,42 @@ | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.default=exports.PropsModel=void 0;var _propsModel=require("./lib/props-model"),PropsModel=_propsModel.PropsModel;exports.PropsModel=PropsModel;var _default=PropsModel;exports.default=_default; | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
exports.default = exports.PropsModel = void 0; | ||
var _propsModel = require("./lib/props-model"); | ||
/** | ||
* The main module for this package, it exports the {@link PropsModel} class. | ||
* | ||
* When used as an ES6 module, you can import as either the default import, or the named 'PropsModel' import, e.g.: | ||
* | ||
* ```javascript | ||
* import PropsModel from 'props-model' | ||
* // or, as a named import: | ||
* import { PropsModel } from 'props-model' | ||
* ``` | ||
* | ||
* When using with node's `require` function, use the `PropsModel` property of the import, e.g.: | ||
* ```javascript | ||
* const PropsModel = require('props-model').PropsModel | ||
* // or, with destructuring assignment: | ||
* const { PropsModel } = require('props-model') | ||
* ``` | ||
* | ||
* @see {@link PropsModel} | ||
* @module props-model | ||
*/ | ||
/** | ||
* The named 'PropsModel' export for the module, which is the same as the default export. | ||
* | ||
* @static | ||
* @type {Constructor} | ||
* @see {@link PropsModel} | ||
*/ | ||
const PropsModel = _propsModel.PropsModel; | ||
exports.PropsModel = PropsModel; | ||
var _default = PropsModel; | ||
exports.default = _default; |
@@ -1,1 +0,755 @@ | ||
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.PropsModel=PropsModel;function _toArray(a){return _arrayWithHoles(a)||_iterableToArray(a)||_nonIterableRest()}function _defineProperty(a,b,c){return b in a?Object.defineProperty(a,b,{value:c,enumerable:!0,configurable:!0,writable:!0}):a[b]=c,a}function _toConsumableArray(a){return _arrayWithoutHoles(a)||_iterableToArray(a)||_nonIterableSpread()}function _nonIterableSpread(){throw new TypeError("Invalid attempt to spread non-iterable instance")}function _iterableToArray(a){if(Symbol.iterator in Object(a)||"[object Arguments]"===Object.prototype.toString.call(a))return Array.from(a)}function _arrayWithoutHoles(a){if(Array.isArray(a)){for(var b=0,c=Array(a.length);b<a.length;b++)c[b]=a[b];return c}}function _slicedToArray(a,b){return _arrayWithHoles(a)||_iterableToArrayLimit(a,b)||_nonIterableRest()}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}function _iterableToArrayLimit(a,b){var c=[],d=!0,e=!1,f=void 0;try{for(var g,h=a[Symbol.iterator]();!(d=(g=h.next()).done)&&(c.push(g.value),!(b&&c.length===b));d=!0);}catch(a){e=!0,f=a}finally{try{d||null==h["return"]||h["return"]()}finally{if(e)throw f}}return c}function _arrayWithHoles(a){if(Array.isArray(a))return a}function PropsModel(a){this._eventEmitter=a,this._props={}}function NOOP(){}function defaultDidChange(a,b){return a!==b}PropsModel.prototype._firePropChangeEvent=function(a,b,c){this._eventEmitter.emit("".concat(a,"-changed"),a,b,c)},PropsModel.prototype.defineProp=function(a,b){var c=2<arguments.length&&void 0!==arguments[2]?arguments[2]:NOOP,d=3<arguments.length&&void 0!==arguments[3]?arguments[3]:defaultDidChange;if(this._props[a])throw new Error("Property already defined: ".concat(a));return this._props[a]={value:b,derived:!1,valueValidator:c,didChange:d},d(b,void 0)&&this._firePropChangeEvent(a,b,void 0),this},PropsModel.prototype.defineDerivedProp=function(a){var b=this,c=1<arguments.length&&void 0!==arguments[1]?arguments[1]:[],d=2<arguments.length?arguments[2]:void 0,e=3<arguments.length?arguments[3]:void 0,f=4<arguments.length&&void 0!==arguments[4]?arguments[4]:defaultDidChange;if(this._props[a])throw new Error("Property already defined: ".concat(a));var g=this.createUtilizer(c,d),h="undefined"==typeof e?g():e;return this._props[a]={value:h,derived:!0,valueValidator:function a(){},didChange:f},this._onAny(function(){},c,function(){b.set(a,g())}),f(h,void 0)&&this._firePropChangeEvent(a,h,void 0),this},PropsModel.prototype._set=function(a){for(var b=this,c=arguments.length,d=Array(1<c?c-1:0),e=1;e<c;e++)d[e-1]=arguments[e];if(1===d.length){var h=[];Object.keys(d[0]).forEach(function(a){if(!b._props[a])throw new Error("No such property '".concat(a,"'"))}),Object.keys(d[0]).forEach(a),Object.entries(d[0]).forEach(function(a){var c=_slicedToArray(a,2),d=c[0],e=c[1];b._props[d].valueValidator(e)}),Object.entries(d[0]).forEach(function(a){var c=_slicedToArray(a,2),d=c[0],e=c[1],f=b._props[d].value;b._props[d].value=e,b._props[d].didChange(e,f)&&h.push([d,e,f])}),h.forEach(function(a){return b._firePropChangeEvent.apply(b,_toConsumableArray(a))})}else{var f=d[0],g=d[1];if(!this._props[f])throw new Error("No such property '".concat(f,"'"));a(f),this._props[f].valueValidator(g);var i=this._props[f].value;this._props[f].value=g,this._props[f].didChange(g,i)&&this._firePropChangeEvent(f,g,i)}},PropsModel.prototype._get=function(a,b){if(a(b),!this._props[b])throw new Error("No such property '".concat(b,"'"));return this._props[b].value},PropsModel.prototype._toJSON=function(a){return Object.entries(this._props).filter(function(b){var c=_slicedToArray(b,1),d=c[0];return a(d)}).reduce(function(a,b){var c=_slicedToArray(b,2),d=c[0],e=c[1].value;return a[d]=e,a},{})};function createAccessorNames(a){for(var b=arguments.length,c=Array(1<b?b-1:0),d=1;d<b;d++)c[d-1]=arguments[d];return c.map(function(b){return"".concat(b).concat(a.replace(/^./,function(a){return a.toUpperCase()}))})}PropsModel.prototype._installAccessors=function(a,b,c,d){for(var e=this,f=Object.keys(d),g=0;g<f.length;g++){var h=f[g],i=d[h];if(!this._props[h])throw new Error("Cannot create accessors for non-existant property '".concat(h,"'"));switch(i.toLowerCase()){case"readonly":a(h);break;case"readwrite":a(h),b(h);break;case"none":break;default:throw new Error("Unknown access type '".concat(i,"' specified for property '").concat(h,"'"));}}for(var j=Object.keys(d),k=function(){var a,b=j[l],f=d[b];switch(f.toLowerCase()){case"readonly":var g=createAccessorNames(b,"get"),h=_slicedToArray(g,1),i=h[0],k=_defineProperty({},i,function(){return e._props[b].value})[i];c[i]=k;break;case"readwrite":var m=createAccessorNames(b,"get","set"),n=_slicedToArray(m,2),o=n[0],p=n[1],q=(a={},_defineProperty(a,o,function(){return e._props[b].value}),_defineProperty(a,p,function(a){e._set(function(){},b,a)}),a);c[o]=q[o],c[p]=q[p];break;default:}},l=0;l<j.length;l++)k()},PropsModel.prototype._createUtilizer=function(a,b,c){var d=this,e=_toArray(b),f=e.slice(0);return f.forEach(a),f.forEach(function(a){if(!d._props[a])throw new Error("Cannot create utilizer of unknown property '".concat(a,"'"))}),function(){for(var a=arguments.length,b=Array(a),e=0;e<a;e++)b[e]=arguments[e];return c.apply(void 0,_toConsumableArray(f.map(function(a){return d._props[a].value})).concat(b))}},PropsModel.prototype._onAny=function(a,b,c){var d=_toArray(b),e=d.slice(0);e.forEach(a);for(var f=0;f<e.length;f++)this._eventEmitter.on("".concat(e[f],"-changed"),c)},PropsModel.prototype._createChangeHandler=function(a,b,c){var d=_toArray(b),e=d.slice(0),f=this._createUtilizer(a,e,c);return this._onAny(function(){},e,function(){return f()}),f},PropsModel.prototype.createUtilizer=function(a,b){return this._createUtilizer(function(){},a,b)},PropsModel.prototype.onAny=function(a,b){return this._onAny(function(){},a,b)},PropsModel.prototype.createChangeHandler=function(a,b){return this._createChangeHandler(function(){},a,b)},PropsModel.prototype.set=function(){for(var a=arguments.length,b=Array(a),c=0;c<a;c++)b[c]=arguments[c];return this._set.apply(this,[function(){}].concat(b))},PropsModel.prototype.get=function(){for(var a=arguments.length,b=Array(a),c=0;c<a;c++)b[c]=arguments[c];return this._get.apply(this,[function(){}].concat(b))},PropsModel.prototype.toJSON=function(){return this._toJSON(function(){return!0})},PropsModel.prototype.installAccessors=function(){for(var a=arguments.length,b=Array(a),c=0;c<a;c++)b[c]=arguments[c];return this._installAccessors.apply(this,[function(){},function(){}].concat(b))};function propertyCheckerToValidator(a){return function(b){var c=a(b);if(c instanceof Error)throw c;else if(!c)throw new Error("Requested access to property '".concat(b,"' is not allowed"))}}PropsModel.prototype.createApi=function(a){var b=this,c=1<arguments.length&&arguments[1]!==void 0?arguments[1]:propertyCheckerToValidator(a),d=2<arguments.length&&arguments[2]!==void 0?arguments[2]:c;return{get:function f(){for(var a=arguments.length,d=Array(a),e=0;e<a;e++)d[e]=arguments[e];return b._get.apply(b,[c].concat(d))},set:function f(){for(var a=arguments.length,c=Array(a),e=0;e<a;e++)c[e]=arguments[e];return b._set.apply(b,[d].concat(c))},createUtilizer:function f(){for(var a=arguments.length,d=Array(a),e=0;e<a;e++)d[e]=arguments[e];return b._createUtilizer.apply(b,[c].concat(d))},createChangeHandler:function f(){for(var a=arguments.length,d=Array(a),e=0;e<a;e++)d[e]=arguments[e];return b._createChangeHandler.apply(b,[c].concat(d))},installAccessors:function g(){for(var a=arguments.length,e=Array(a),f=0;f<a;f++)e[f]=arguments[f];return b._installAccessors.apply(b,[c,d].concat(e))},toJSON:function c(){return b._toJSON(a)}}};function propNameIsPublic(a){return!a.startsWith("_")}function assertPropNameIsPublic(a){if(!propNameIsPublic(a))throw new Error("Property is not publicly accessible: ".concat(a))}function createStandardWriteValidator(a){return function(b){if(a._props[b].derived)throw new Error("Write access to ".concat(b," is not allowed because the property is a derived property."))}}PropsModel.prototype.getStandardPublicApi=function(){var a=createStandardWriteValidator(this);return this.createApi(propNameIsPublic,assertPropNameIsPublic,function(b){assertPropNameIsPublic(b),a(b)})},PropsModel.prototype.getStandardPrivateApi=function(){return this.createApi(function(){return!0},function(){},createStandardWriteValidator(this))}; | ||
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
exports.PropsModel = void 0; | ||
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } | ||
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } | ||
function _iterableToArrayLimit(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } | ||
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } | ||
/** | ||
* Instances of this class are used to configure and manage a set of named properties. | ||
* Properties can have their values set and retrieved, and they fire change events through | ||
* the given [EventEmitter]{@link external:EventEmitter} when the values change. | ||
* | ||
* Each property has a unique name, defined by a string. Properties can either be _primary_ or | ||
* _derived_. A **primary** property is one that you have to set a value for explicitly. A **derived** | ||
* property is calcuated automatically from the values of other properties (either primary or derived). | ||
* Derived properties are registered as change-listeners to all the properties they are calculated | ||
* from, so that they are automatically updated when any of their dependencies change. Derived properties | ||
* likewise fire property change events when their value changes. | ||
* | ||
* @extends PropsModelApi | ||
*/ | ||
class PropsModel { | ||
/** | ||
* @param {external:EventEmitter} eventEmitter The event emitter on which property change events will be | ||
* emitted and listened to. | ||
*/ | ||
constructor(eventEmitter) { | ||
this._eventEmitter = eventEmitter; | ||
this._props = {}; | ||
} | ||
/** | ||
* Helper method that is used to fire a property change event. This should be called *after* the properties | ||
* value has been updated. Also note that the standard [EventEmitter]{@link external:EventEmitter} fires events | ||
* and triggers listeners synchronously, so this won't return until all listeners have acted. This could lead | ||
* to a deep call stack if those listeners end up updating other properties, and so on. | ||
* | ||
* @private | ||
* @param {string} propName The name of hte property which has changed. | ||
* @param {*} newValue The new value of the property. | ||
* @param {*} oldValue The previous value of the property. | ||
*/ | ||
_firePropChangeEvent(propName, newValue, oldValue) { | ||
this._eventEmitter.emit(`${propName}-changed`, propName, newValue, oldValue); | ||
} | ||
/** | ||
* Define a primary property that the model will track. It's value is set to the `initialValue`, which counts as setting | ||
* the value of the property, changing it from `undefined`, so a change event for the property is fired unless `didChange` | ||
* returns a falsey value for the change. | ||
* | ||
* @param {string} propName The name of the property. An Error will be thrown if the property is already defined. | ||
* | ||
* @param {*} initialValue The value to set the property to. | ||
* | ||
* @param {valueValidator} [valueValidator] An optional validator function that will be used whenever the property is set. | ||
* The default allows all values. **Note** that this is _not_ called for the initial value, it's up to you to ensure that | ||
* the initial value is valid. | ||
* | ||
* @param {didChange} [didChange] An optional function that is called anytime the property value is set, to determine | ||
* whether or not the old value and new value should be considered a change. A change event for the property is fired if and only | ||
* if the function returns a truthy value. The default uses `newValue !== oldValue`. Note that the property value is changed | ||
* regardless of what this function returns, it is only used to determine if the event should be fired. | ||
*/ | ||
defineProp(propName, initialValue, valueValidator = NOOP, didChange = defaultDidChange) { | ||
if (this._props[propName]) { | ||
throw new Error(`Property already defined: ${propName}`); | ||
} | ||
this._props[propName] = { | ||
value: initialValue, | ||
derived: false, | ||
valueValidator, | ||
didChange | ||
}; | ||
if (didChange(initialValue, undefined)) { | ||
this._firePropChangeEvent(propName, initialValue, undefined); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Defines a derived property which is set and updated automatically based on the values other specified properties. | ||
* | ||
* @param {string} propName The name of the property to define. An error will be thrown if the property already exists. | ||
* @param {Array<string>} dependsOn The list of property names that this derived property depends on. Note that | ||
* derived properties can only depend on already existing properties, and the dependencies of a derived property cannot | ||
* be changed once set, so it is not possible to create cycles of dependent properties. | ||
* @param {function(...*):*} calculateValue A function that will be invoked as needed to calculate the value of this | ||
* property. It is invoked immediately to set the initial value (unless `initialValue` is given), and invoked again | ||
* anytime one of the specified properties fires a change event. It is invoked with the contemporary values of the | ||
* named properties, each passed as a separate arg, in the order specified in `dependsOn`. | ||
* @param {*} [initialValue] An optional initial value to use, _in place of_ calculating the value. This will | ||
* be used unless it has a `typeof` equal to `'undefined'`. | ||
* @param {didChange} [didChange] An optional function to determine if a new value for the property should be | ||
* considered a change from its previous value. See the same parameter on [`defineProp`]{@link PropsModel#defineProp}. | ||
*/ | ||
defineDerivedProp(propName, dependsOn = [], _calculateValue, initialValue, didChange = defaultDidChange) { | ||
if (this._props[propName]) { | ||
throw new Error(`Property already defined: ${propName}`); | ||
} | ||
const calculateValue = this.createUtilizer(dependsOn, _calculateValue); | ||
const value = typeof initialValue === 'undefined' ? calculateValue() : initialValue; | ||
this._props[propName] = { | ||
value, | ||
derived: true, | ||
valueValidator: () => {}, | ||
didChange | ||
}; | ||
this._onAny(() => {}, dependsOn, () => { | ||
this.set(propName, calculateValue()); | ||
}); | ||
if (didChange(value, undefined)) { | ||
this._firePropChangeEvent(propName, value, undefined); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Create a special derived property that produces a backed view of another property, called the base property. | ||
* Like any derived property, changing the base property will cause a new value fo the view property to be calculated | ||
* and set. Unlike a normal derived property, you can also set the value of the view property and it will be reflected | ||
* in the base property. | ||
* | ||
* In otherwords, you're creating a circular dependency between the view prop and the base prop. We ensure this doesn't | ||
* lead (directly) to an infinite loop by supressing the update of the view prop in response to consequent | ||
* updating of the base prop. However, that means that view and base prop might get out of sync; you need to make sure | ||
* your reduceBaseValue function produces a value for the base property that would yield the same view value. | ||
* | ||
* @param {string} viewName The name of the view-prop to define | ||
* @param {string} viewOf The name of a property that this is a view of | ||
* @param {function(B):V} calculateViewValue A function that calculates the value of the view prop from the value | ||
* of the `viewOf` property. | ||
* @param {function(V, B):B} reduceBaseValue A function which is called whenever the view property is get the updated | ||
* value of the base property. It's invoked with the new value of the view prop and the current value of the base prop. | ||
* @param {function(V, V):boolean} didChange An optional function used to determine if the view prop should be considered | ||
* to be changed. | ||
*/ | ||
definePropView(viewName, viewOf, calculateViewValue, reduceBaseValue, didChange = defaultDidChange) { | ||
if (this._props[viewName]) { | ||
throw new Error(`Property already defined: ${viewName}`); | ||
} | ||
const calculateValue = this.createUtilizer([viewOf], calculateViewValue); | ||
const value = calculateValue(); | ||
this._props[viewName] = { | ||
value, | ||
derived: true, | ||
valueValidator: () => {}, | ||
didChange | ||
}; | ||
let triggered = false; | ||
this._onAny(() => {}, [viewOf], () => { | ||
if (!triggered) { | ||
this.set(viewName, calculateValue()); | ||
} | ||
}); | ||
this._onAny(() => {}, [viewName], (ignore, newViewValue, oldViewValue) => { | ||
const newViewOfValue = reduceBaseValue(newViewValue, this.get(viewOf)); | ||
triggered = true; | ||
this.set(viewOf, newViewOfValue); | ||
triggered = false; | ||
}); | ||
if (didChange(value, undefined)) { | ||
this._firePropChangeEvent(viewName, value, undefined); | ||
} | ||
return this; | ||
} | ||
/** | ||
* Set one or more properties. You won't typically call this directly, you would use it through | ||
* the [set()]{@link PropsModelApi#set} method. | ||
* | ||
* @private | ||
* @param {propValidator} propValidator Called to verify write access to each property attempting | ||
* to be set. | ||
* @param {...*} args There are two signatures available: provide the property name and | ||
* value as two arguments, or provide an object whose property names and property values | ||
* describe what properties you want to set, and how. See {@link PropsModelApi#set(1)} | ||
* and {@link PropsModelApi#set(2)}. | ||
*/ | ||
_set(propValidator, ...args) { | ||
if (args.length === 1) { | ||
Object.keys(args[0]).forEach(propName => { | ||
if (!this._props[propName]) { | ||
throw new Error(`No such property '${propName}'`); | ||
} | ||
}); | ||
Object.keys(args[0]).forEach(propValidator); | ||
Object.entries(args[0]).forEach(([propName, value]) => { | ||
this._props[propName].valueValidator(value); | ||
}); | ||
const oldValues = []; | ||
Object.entries(args[0]).forEach(([propName, value]) => { | ||
oldValues.push(this._props[propName].value); | ||
this._props[propName].value = value; | ||
}); | ||
Object.entries(args[0]).forEach(([propName, value], idx) => { | ||
const oldValue = oldValues[idx]; | ||
if (this._props[propName].didChange(value, oldValue)) { | ||
this._firePropChangeEvent(propName, value, oldValue); | ||
} | ||
}); | ||
} else { | ||
const propName = args[0], | ||
value = args[1]; | ||
if (!this._props[propName]) { | ||
throw new Error(`No such property '${propName}'`); | ||
} | ||
propValidator(propName); | ||
this._props[propName].valueValidator(value); | ||
const oldValue = this._props[propName].value; | ||
this._props[propName].value = value; | ||
if (this._props[propName].didChange(value, oldValue)) { | ||
this._firePropChangeEvent(propName, value, oldValue); | ||
} | ||
} | ||
} | ||
/** | ||
* Get the value of the named property. You won't typically call this directly, you would use it through | ||
* the [set()]{@link PropsModelApi#get} method. | ||
* | ||
* @private | ||
* @param {propValidator} propValidator Called to verify read access to the named property. | ||
* @param {string} propName The name of the property to get the value of. | ||
* | ||
* @returns {*} The value of the requested property | ||
* @throws {Error} If the named property does not exist. | ||
* @throws {*} Anything thrown by the `propValidator` when invoked with the given `propName`. | ||
*/ | ||
_get(propValidator, propName) { | ||
propValidator(propName); | ||
if (!this._props[propName]) { | ||
throw new Error(`No such property '${propName}'`); | ||
} | ||
return this._props[propName].value; | ||
} | ||
/** | ||
* Return an JSON-serializable object that represents properties tracked by this model and their | ||
* values. You won't typically call this directly, you would use it through | ||
* the [set()]{@link PropsModelApi#toJSON} method. | ||
* | ||
* The name of each property known to this model is checked against the given `propChecker`; if | ||
* it returns a truthy value, the property will be included in the returned "JSON" object, otherwise | ||
* it wil not be. | ||
* | ||
* Note that property values are passed through `JSON.stringify` and then `JSON.parse` before being | ||
* attached to the returned object. This may or may not lead to a different instance than what | ||
* is kept in the model, depending on how the object handles JSONification, which could leak a | ||
* reference to a mutable shared object, possibly allowing write unintended modifications without | ||
* access restriction or event firing. | ||
* | ||
* @private | ||
* @param {propChecker} propChecker Called to determine which properties should be included | ||
* in the returned object. | ||
* @returns {*} An object representing a JSONable account of the permitted properties and their values. | ||
*/ | ||
_toJSON(propChecker) { | ||
return Object.entries(this._props).filter(([propName]) => propChecker(propName)).reduce((o, [propName, { | ||
value | ||
}]) => { | ||
o[propName] = JSON.parse(JSON.stringify(value)); | ||
return o; | ||
}, {}); | ||
} | ||
/** | ||
* XXX Left off documenting here. | ||
* Adds accessor methods (getters and setters) fo the specified properties as methods on the given target object. | ||
*/ | ||
_installAccessors(readValidator, writeValidator, target, propertyAccess) { | ||
var _arr = Object.keys(propertyAccess); | ||
for (var _i = 0; _i < _arr.length; _i++) { | ||
let propName = _arr[_i]; | ||
const access = propertyAccess[propName]; | ||
if (!this._props[propName]) { | ||
throw new Error(`Cannot create accessors for non-existant property '${propName}'`); | ||
} | ||
switch (access.toLowerCase()) { | ||
case 'readonly': | ||
readValidator(propName); | ||
break; | ||
case 'readwrite': | ||
readValidator(propName); | ||
writeValidator(propName); | ||
break; | ||
case 'none': | ||
break; | ||
default: | ||
throw new Error(`Unknown access type '${access}' specified for property '${propName}'`); | ||
} | ||
} | ||
var _arr2 = Object.keys(propertyAccess); | ||
for (var _i2 = 0; _i2 < _arr2.length; _i2++) { | ||
let propName = _arr2[_i2]; | ||
const access = propertyAccess[propName]; | ||
switch (access.toLowerCase()) { | ||
case 'readonly': | ||
const _createAccessorNames = createAccessorNames(propName, 'get'), | ||
_createAccessorNames2 = _slicedToArray(_createAccessorNames, 1), | ||
funcName = _createAccessorNames2[0]; | ||
const getter = { | ||
[funcName]: () => { | ||
return this._props[propName].value; | ||
} | ||
}[funcName]; | ||
target[funcName] = getter; | ||
break; | ||
case 'readwrite': | ||
const _createAccessorNames3 = createAccessorNames(propName, 'get', 'set'), | ||
_createAccessorNames4 = _slicedToArray(_createAccessorNames3, 2), | ||
getterName = _createAccessorNames4[0], | ||
setterName = _createAccessorNames4[1]; | ||
const funcs = { | ||
[getterName]: () => { | ||
return this._props[propName].value; | ||
}, | ||
[setterName]: value => { | ||
this._set(() => {}, propName, value); | ||
} | ||
}; | ||
target[getterName] = funcs[getterName]; | ||
target[setterName] = funcs[setterName]; | ||
break; | ||
default: | ||
break; | ||
} | ||
} | ||
} | ||
/** | ||
* Create a function that will delegate to the given handler with the values of specified properties as the arguments. | ||
* The returned function will fetch the values of the named properties and pass them in the order given as the first | ||
* arguments to the given handler function. Any arguments passed to the returned function will be passed as additional | ||
* arguments to handler. | ||
* | ||
* @param {Array<string>} propNames The array of property names you want to utilize | ||
* @param {function} handler The function that the returned function will delegate to with the values of the specified | ||
* properties. | ||
*/ | ||
_createUtilizer(propValidator, [...propNames], handler) { | ||
propNames.forEach(propValidator); | ||
propNames.forEach(propName => { | ||
if (!this._props[propName]) { | ||
throw new Error(`Cannot create utilizer of unknown property '${propName}'`); | ||
} | ||
}); | ||
return (...args) => { | ||
return handler(...propNames.map(propName => this._props[propName].value), ...args); | ||
}; | ||
} | ||
/** | ||
* Register the given handler to be invoked any time any of the given properties fire a change event. | ||
* | ||
* The given handler is invoked with three arguments: propName, newValue, oldValue. | ||
*/ | ||
_onAny(propValidator, [...propNames], handler) { | ||
propNames.forEach(propValidator); | ||
for (let i = 0; i < propNames.length; i++) { | ||
this._eventEmitter.on(`${propNames[i]}-changed`, handler); | ||
} | ||
} | ||
/** | ||
* Register the given handler to be called with the values of all of the named properties anytime | ||
* any one of those properties changes. | ||
* | ||
* This uses {@link #_createUtilizer} to create a no-argument function that will collect the | ||
* values of the properties and delegate them to the given `handler`. The function thus prouced is | ||
* registered as a change handler for the given properties, and is also returned from this function. | ||
*/ | ||
_createChangeHandler(propValidator, [...respondsTo], handler) { | ||
const callback = this._createUtilizer(propValidator, respondsTo, handler); | ||
this._onAny(() => {}, respondsTo, callback); | ||
return callback; | ||
} | ||
createUtilizer(propNames, handler) { | ||
return this._createUtilizer(() => {}, propNames, handler); | ||
} | ||
onAny(propNames, handler) { | ||
return this._onAny(() => {}, propNames, handler); | ||
} | ||
/** | ||
* Like {@link #createUtilizer}, but it also registers the created utilizer function to be called anytime | ||
* any of the specified properties change. The utilizer function is still returned. | ||
* | ||
* @param {Array<string>} respondsTo The array of property names to respond to | ||
* @param {function} handler The handler to all when ay of the specified properties change | ||
*/ | ||
createChangeHandler(respondsTo, handler) { | ||
return this._createChangeHandler(() => {}, respondsTo, handler); | ||
} | ||
set(...args) { | ||
return this._set(() => {}, ...args); | ||
} | ||
get(...args) { | ||
return this._get(() => {}, ...args); | ||
} | ||
toJSON() { | ||
return this._toJSON(() => true); | ||
} | ||
installAccessors(...args) { | ||
return this._installAccessors(() => {}, () => {}, ...args); | ||
} | ||
/** | ||
* Create an API object that provides limited access to this model defined by the given checkers and validators. | ||
* | ||
* @param {function(string):boolean|function(string):Error} readChecker A function to determine whether or not the API should have | ||
* read access to a given property name. Invoked with a property name, it should return a non-Error truthy value if the API | ||
* should have read access to the property, and either a falsey value or an Error object if not. | ||
* @param {function(string):*} [readValidator] A function to enforce read access; it is invoked with a property name and should | ||
* throw an Error if and only if the API should not have read access to the named property. The return value is ignored. If this | ||
* argument is not provided, a default is derived from the `readChecker`. | ||
* @param {function(string):*} [writeValidator=readValidator] A function to enforce write access, similar to the `readValidator`. | ||
* If not given, the default is to use the `readValidator`. | ||
* | ||
* @returns {{get, set, createUtilizer, createChangeHandler, toJSON}} | ||
*/ | ||
createApi(readChecker, readValidator = propertyCheckerToValidator(readChecker), writeValidator = readValidator) { | ||
return { | ||
get: (...args) => this._get(readValidator, ...args), | ||
set: (...args) => this._set(writeValidator, ...args), | ||
createUtilizer: (...args) => this._createUtilizer(readValidator, ...args), | ||
createChangeHandler: (...args) => this._createChangeHandler(readValidator, ...args), | ||
installAccessors: (...args) => this._installAccessors(readValidator, writeValidator, ...args), | ||
toJSON: () => this._toJSON(readChecker) | ||
}; | ||
} | ||
/** | ||
* Returns an API object that has read access to all public properties, and write access to all public | ||
* properties that are not derived. | ||
* | ||
* Public properties are those whose name does not begin with an underscore | ||
* | ||
* @see #createApi | ||
*/ | ||
getStandardPublicApi() { | ||
const standardWriteValidator = createStandardWriteValidator(this); | ||
return this.createApi(propNameIsPublic, assertPropNameIsPublic, propName => { | ||
assertPropNameIsPublic(propName); | ||
standardWriteValidator(propName); | ||
}); | ||
} | ||
/** | ||
* Returns an API object that allows read access to all properties, and write access | ||
* to all non-derived properties. | ||
* | ||
* This is typically the API used by the property owner itself, to ensure you aren't | ||
* trying to write to derived properties, which is usually not recommended. | ||
*/ | ||
getStandardPrivateApi() { | ||
return this.createApi(() => true, () => {}, createStandardWriteValidator(this)); | ||
} | ||
} | ||
exports.PropsModel = PropsModel; | ||
function NOOP() {} | ||
function defaultDidChange(newValue, oldVaue) { | ||
return newValue !== oldVaue; | ||
} | ||
function createAccessorNames(propName, ...prefixes) { | ||
return prefixes.map(prefix => `${prefix}${propName.replace(/^./, c => c.toUpperCase())}`); | ||
} | ||
function propertyCheckerToValidator(checker) { | ||
return propName => { | ||
const checkResult = checker(propName); | ||
if (checkResult instanceof Error) { | ||
throw checkResult; | ||
} else if (!checkResult) { | ||
throw new Error(`Requested access to property '${propName}' is not allowed`); | ||
} | ||
}; | ||
} | ||
function propNameIsPublic(propName) { | ||
return !propName.startsWith('_'); | ||
} | ||
function assertPropNameIsPublic(propName) { | ||
if (!propNameIsPublic(propName)) { | ||
throw new Error(`Property is not publicly accessible: ${propName}`); | ||
} | ||
} | ||
function createStandardWriteValidator(propModel) { | ||
return propName => { | ||
if (propModel._props[propName].derived) { | ||
throw new Error(`Write access to ${propName} is not allowed because the property is a derived property.`); | ||
} | ||
}; | ||
} | ||
/** | ||
* Handles emitting and subscribing to events. Used for handling property change events by the {@link PropsModel}. | ||
* @external EventEmitter | ||
* @type {Constructor} | ||
* @see https://nodejs.org/api/events.html#events_class_eventemitter | ||
*/ | ||
/** | ||
* A function that can be defined for each primary property to determine whether or not the property can | ||
* be set to a given value. The value is considered valid unless the validator throws. | ||
* | ||
* Any return value is ignored. | ||
* | ||
* @callback valueValidator | ||
* @param {*} incomingValue The value we want to set the property to. | ||
* @throws {*} If the function throws anything, the incoming value is considered invalid and the property | ||
* value will not be set, nor will a property change event be fired for it. | ||
*/ | ||
/** | ||
* A function that is used to determine if a property should be considered to have changed, given the | ||
* old and new values. This is used to determine whether or not property change event will be fired. | ||
* | ||
* This is useful if you have multiple valid ways to represent the same canonical value, and don't | ||
* want to cfire a change event unnecessarily. For instance, if the value of the property is an object, | ||
* then two different objects which have all the same contents _might_ be considered equivalent and | ||
* not treated as a change. | ||
* | ||
* **NB**: Keep in mind that non-primitive property values can be changed out from under you by anyone | ||
* who still has a reference to it. So even though an object or array might look the same as the current | ||
* value when it's passed in, that doesn't mean it will remain the same. | ||
* | ||
* @callback didChange | ||
* @param {*} newValue The new value of the property, to which it was changed. | ||
* @param {*} prevValue The previous value of the property, from which it was changed. | ||
* @return {boolean} Any truthy value will indicate that the property should be considered changed. | ||
*/ | ||
/** | ||
* A generic validator that is typically used to enforce access authorization for properties based | ||
* on their names. | ||
* | ||
* @callback propValidator | ||
* @param {string} propName The name of the property | ||
* @throws {*} Throw an error if the property name is not valid for the appropriate task. | ||
*/ | ||
/** | ||
* A generic checker function that is typically used to determine access authorization | ||
* for properties. Contrasted with a {@link propValidator}, this is not intended to make | ||
* any assertions, i.e., it is not meant to throw access to the property is not allowed, | ||
* but simply to return a falsey value in that case. | ||
* | ||
* @callback propChecker | ||
* @param {string} propName the name of the property | ||
* @returns {boolean} A truthy value if and only if the access should be granted to the named | ||
* property, a falsey value otherwise. | ||
*/ | ||
/** | ||
* A common interface for manipulating and using (but not defining) properties. | ||
* The {@link PropsModel} class implements this interface, it also provides methods for | ||
* getting other implementations of this interface for various access limitations. | ||
* See, for instance, {@link PropsModel#createApi}. | ||
* | ||
* @interface PropsModelApi | ||
*/ | ||
/** | ||
* Set a single property to a new value. The given value will be passed to the configured {@link valueValidator} | ||
* for the property, _before_ the property is set; if the validator throws an error, the error will not be | ||
* caught, and the property will not be updated. | ||
* | ||
* After the value is updated, its configured {@link didChange} function will be called and a change event | ||
* will be fired unless `didChange` returns a falsey value. | ||
* | ||
* @method set(1) | ||
* @inner | ||
* @memberof PropsModelApi | ||
* @param {string} propName The name of the property | ||
* @param {*} value The new value. | ||
*/ | ||
/** | ||
* Atomically set multiple properties at once. See [`set(string, *)`]{@link PropsModelApi#set(1)} for general information. | ||
* This variant sets all the properties specified as keys to the given `propValues` object, setting each | ||
* to the corresponding value. Note that all properties and values are validated _before_ any property | ||
* is changed. This includes ensuring that the property is accessible for the given API, that the property | ||
* exists, and that the value is valid according to the property's {@link valueValidator}. | ||
* | ||
* Additionally, all properties are updated _before_ any property change events are fired. Events are fired | ||
* individually for each property, in the order they iterate from `propVaues`, and subject to that properties | ||
* {@link didChange} function. | ||
* | ||
* @method set(2) | ||
* @inner | ||
* @memberof PropsModelApi | ||
* @param {object} propValues An object mapping property names to the values you want to set them to. | ||
* All own-properties of the object are assumed to be property names you want to set. | ||
*/ | ||
/** | ||
* Get the value of the named property. Throws an error if the property does not exist or the | ||
* API doesn't have read access to it. | ||
* | ||
* @method get | ||
* @inner | ||
* @memberof PropsModelApi | ||
* @param {string} propName The name of the property to get | ||
* @returns {*} The value of the named property. | ||
*/ | ||
/** | ||
* Create a "utilizer function" that makes use of the values of all the named properties. | ||
* | ||
* @method createUtilizer | ||
* @inner | ||
* @memberof PropsModelApi | ||
* @param {Array<string>} propNames A list of property names that the utilizer will use. | ||
* @param {function(...*):*} handler The function that the returned utilizer function will | ||
* delegate to, invoked with the contemporary values of the named properties, each passed | ||
* as an individual argument, in the same order they're given in `propNames`. Any arguments passed | ||
* to the utlizer function will be also be passed, following the property values. | ||
* | ||
* @throws {Error} If any of the named properties either don't exist or aren't accessible to the | ||
* API at the time this funciton is called. | ||
* | ||
* @returns {function(...args):*} Returns a "utilizer" function, which can be invoked to get the | ||
* values of the properties named by `propNames` and pass them to the given `handler` function, | ||
* along with any args passed to the utilizer function. The utilizer function delegates to | ||
* the `handler` at that point, returning whatever value it returns. | ||
*/ | ||
/** | ||
* Register the given `handler` to be called anytime one of the properties specified by `propNames` | ||
* fires a change event. The `handler` is invoked with the current values of all the specified | ||
* properties, followed by the standard change-event listener arguments: propertyName, newValue, oldValue | ||
* for the property that was changed. Note that there is _no aggregation_ of change events, so it | ||
* something changes multiple properties at once, the handler will be invoked for each property change. | ||
* | ||
* This actually uses [createUtilizer()]{@link PropsModelApi~createUtilizer} to create and returns a utilizer function, after registering | ||
* the utilizer for the change events. | ||
* | ||
* @method createChangeHandler | ||
* @inner | ||
* @memberof PropsModelApi | ||
* @param {Array<string>} propNames A list of property names that the utilizer will use. | ||
* @param {function(...*):*} handler The function that the returned utilizer function will | ||
* | ||
* @throws {Error} If any of the named properties either don't exist or aren't accessible to the | ||
* API at the time this funciton is called. | ||
* | ||
* @returns {function(...args):*} Returns a "utilizer" function, which can be invoked to get the | ||
* values of the properties named by `propNames` and pass them to the given `handler` function, | ||
* along with any args passed to the utilizer function. The utilizer function delegates to | ||
* the `handler` at that point, returning whatever value it returns. | ||
*/ | ||
/** | ||
* Creates accessor functions (getter and/or setters) for specific properties and attaches them | ||
* as methods to the given `target` object. This is a usefull way to create a bean-type | ||
* interface for your properties. | ||
* | ||
* Accessor function names (and the property names with which they attach to target) take the form | ||
* "get${PropName}" for getters and "set${PropName}" for setters, where `${PropName}` is simply the | ||
* properties name with the first character capitalized. | ||
* | ||
* @method installAccessors | ||
* @inner | ||
* @memberof PropsModelApi | ||
* @param {object} target The object onto which the accessor methods will be attached as properties. | ||
* @param {object} propertyAccess An object describing which properties to create accessors for, and | ||
* what accessors to create. Each own-property of the object is the name of a property, and the corresponding | ||
* property value should be one of `'readonly'`, `'readwrite'`, or `'none'`, to create a getter only, | ||
* a getter and a setter, or no accessors, respectively. The `'none'` value is only to be explicit, if | ||
* the property is not included in the object, no accessors will be created for it. Any other property | ||
* values will cause an error, as will properties of this object which do not correspond to known property | ||
* names or which correspond to properties the API doesn't have appropriate access to. | ||
*/ | ||
/** | ||
* Returns an object representing the properties and their current values that this API has | ||
* read access to. | ||
* | ||
* @method toJSON | ||
* @inner | ||
* @memberof PropsModelApi | ||
*/ |
{ | ||
"name": "props-model", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "A model for properties including change events and derived properties", | ||
@@ -46,4 +46,4 @@ "main": "dist/index.js", | ||
"verify": "npm-run-all check test", | ||
"compile": "if-env NODE_ENV=production && babel --presets minify --no-comments src -d dist || babel src -d dist", | ||
"docs": "jsdoc --recurse --destination out/jsdocs --package package.json --configure jsdoc.conf.json \"src/**/*\"", | ||
"compile": "babel src -d dist", | ||
"docs": "jsdoc --recurse --readme README.md --destination out/jsdocs --configure jsdoc.conf.json --package package.json src/", | ||
"build": "npm-run-all clean verify compile", | ||
@@ -70,3 +70,2 @@ "prepublishOnly": "cross-env NODE_ENV=production npm run build" | ||
"@babel/register": "7.0.0", | ||
"babel-preset-minify": "0.5.0", | ||
"chai": "4.2.0", | ||
@@ -76,15 +75,14 @@ "cross-env": "5.2.0", | ||
"jsdoc": "3.5.5", | ||
"jsdoc-to-markdown": "4.0.1", | ||
"mkdirp": "0.5.1", | ||
"mocha": "5.2.0", | ||
"npm-package-json-lint": "3.3.0", | ||
"npm-package-json-lint": "3.4.1", | ||
"npm-run-all": "4.1.5", | ||
"nyc": "13.1.0", | ||
"remark-cli": "5.0.0", | ||
"nyc": "13.3.0", | ||
"remark-cli": "6.0.1", | ||
"rimraf": "2.6.2", | ||
"sinon": "7.1.1", | ||
"sinon": "7.2.0", | ||
"sinon-chai": "3.3.0", | ||
"snazzy": "7.1.1", | ||
"snazzy": "8.0.0", | ||
"standard": "12.0.1" | ||
} | ||
} |
@@ -5,2 +5,17 @@ # props-model | ||
[![JavaScript Style Guide](https://cdn.rawgit.com/standard/standard/master/badge.svg)](https://github.com/standard/standard) | ||
[![npm](https://img.shields.io/npm/v/props-model.svg)](https://libraries.io/github/mearns/props-model) | ||
[![Stable Build](https://travis-ci.org/mearns/props-model.svg?branch=versions%2Fstable)](https://travis-ci.org/mearns/props-model) | ||
[![node](https://img.shields.io/node/v/props-model.svg)](https://www.npmjs.com/package/props-model) | ||
[![NpmLicense](https://img.shields.io/npm/l/props-model.svg)](https://spdx.org/licenses/MIT) | ||
[![npm bundle size (minified)](https://img.shields.io/bundlephobia/min/props-model.svg)](https://www.npmjs.com/package/props-model) | ||
[![Libraries.io for GitHub](https://img.shields.io/librariesio/github/mearns/props-model.svg)](https://libraries.io/github/mearns/props-model) | ||
[![npm](https://img.shields.io/npm/dy/props-model.svg)](https://www.npmjs.com/package/props-model) | ||
[![GitHub issues](https://img.shields.io/github/issues-raw/mearns/props-model.svg)](https://github.com/mearns/props-model/issues?q=is%3Aissue+is%3Aopen) | ||
[![GitHub pull requests](https://img.shields.io/github/issues-pr-raw/mearns/props-model.svg)](https://github.com/mearns/props-model/pulls?q=is%3Apr+is%3Aopen) | ||
[![GitHub last commit](https://img.shields.io/github/last-commit/mearns/props-model.svg)](https://github.com/mearns/props-model/commits/) | ||
## Overview | ||
@@ -7,0 +22,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Empty package
Supply chain riskPackage does not contain any code. It may be removed, is name squatting, or the result of a faulty package publish.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
41310
20
14
692
1
110