class-autobind-decorator
Advanced tools
Comparing version 2.3.0 to 3.0.0
@@ -1,115 +0,221 @@ | ||
'use strict'; | ||
(function (global, factory) { | ||
if (typeof define === "function" && define.amd) { | ||
define(['exports', './lib/ReactComponentSpecMethods', './lib/isObject', './lib/uniqueConcatArrays'], factory); | ||
} else if (typeof exports !== "undefined") { | ||
factory(exports, require('./lib/ReactComponentSpecMethods'), require('./lib/isObject'), require('./lib/uniqueConcatArrays')); | ||
} else { | ||
var mod = { | ||
exports: {} | ||
}; | ||
factory(mod.exports, global.ReactComponentSpecMethods, global.isObject, global.uniqueConcatArrays); | ||
global.index = mod.exports; | ||
} | ||
})(this, function (exports, _ReactComponentSpecMethods, _isObject, _uniqueConcatArrays) { | ||
'use strict'; | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
Object.defineProperty(exports, "__esModule", { | ||
value: true | ||
}); | ||
exports.autoBindMethods = undefined; | ||
exports.default = autoBindMethods; | ||
exports.autoBindMethodsForReact = autoBindMethodsForReact; | ||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; | ||
var _ReactComponentSpecMethods2 = _interopRequireDefault(_ReactComponentSpecMethods); | ||
exports.default = autoBindMethods; | ||
/** | ||
* Overloaded function for auto-binding the methods of a class to the class's relevant instance. If | ||
* the first argument is a function, as it will be when this is used as a "bare" or "unconfigured" | ||
* decorator -- as in `@autoBindMethods class SomeClass {}` -- it does the auto-bind decorating by | ||
* delegating to `autoBindMethodsDecorator`. If the first argument is *not* a function, as happens | ||
* when this is used as a "configured" decorator -- as in `@autoBindMethods(options) class SomeClass | ||
* {}` -- it returns a function that *itself* accepts a function (the class constructor) as its | ||
* first argument and that does the auto-bind decorating by delegating to | ||
* `autoBindMethodsDecorator`. | ||
* | ||
* The delegate method `autoBindMethodsDecorator` is `call`ed in order to avoid changing the context | ||
* from whatever it would ordinarily be in the case of a non-overloaded decorator, while still | ||
* allowing us to pass on any received `options`. | ||
* | ||
* @param {Object} [options] - optional options | ||
* @param {String[]} [options.methodsToIgnore] - names of methods to skip auto-binding | ||
* @param {boolean} [options.dontOptimize] - if truthy, turns off the decorator's default | ||
* optimization behavior, which is to define the bound method directly on the class instance | ||
* in order to prevent lookups and re-binding on every access | ||
* @returns {*} | ||
*/ | ||
function autoBindMethods(input) { | ||
if (typeof input !== 'function') { | ||
return function (target) { | ||
autoBindMethodsDecorator.call(this, target, input); | ||
var _isObject2 = _interopRequireDefault(_isObject); | ||
var _uniqueConcatArrays2 = _interopRequireDefault(_uniqueConcatArrays); | ||
function _interopRequireDefault(obj) { | ||
return obj && obj.__esModule ? obj : { | ||
default: obj | ||
}; | ||
} | ||
autoBindMethodsDecorator.call(this, input); | ||
} | ||
var _extends = Object.assign || function (target) { | ||
for (var i = 1; i < arguments.length; i++) { | ||
var source = arguments[i]; | ||
/** | ||
* A "legacy"-style "class" decorator function for auto-binding the methods of the "class." | ||
* | ||
* @param {Function} target - an ES2015 "class" or -- what is effectively the same thing -- a | ||
* constructor function. | ||
* @param {Object} [options] - optional options | ||
* @param {string[]} [options.methodsToIgnore] - names of methods to skip auto-binding | ||
* @param {boolean} [options.dontOptimize] - if truthy, turns off the decorator's default | ||
* optimization behavior, which is to define the bound method directly on the class instance | ||
* in order to prevent lookups and re-binding on every access | ||
*/ | ||
function autoBindMethodsDecorator(target) { | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
for (var key in source) { | ||
if (Object.prototype.hasOwnProperty.call(source, key)) { | ||
target[key] = source[key]; | ||
} | ||
} | ||
} | ||
if (typeof target !== 'function') { | ||
throw new TypeError('The autoBindMethods decorator must be passed a function as the first argument. ' + ('It received an argument of type ' + (typeof target === 'undefined' ? 'undefined' : _typeof(target)) + '.')); | ||
return target; | ||
}; | ||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { | ||
return typeof obj; | ||
} : function (obj) { | ||
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; | ||
}; | ||
/** | ||
* Overloaded function for auto-binding the methods of a class to the class's relevant instance. If | ||
* the first argument is a function, as it will be when this is used as a "bare" or "unconfigured" | ||
* decorator -- as in `@autoBindMethods class SomeClass {}` -- it does the auto-bind decorating by | ||
* delegating to `autoBindMethodsDecorator`. If the first argument is *not* a function, as happens | ||
* when this is used as a "configured" decorator -- as in `@autoBindMethods(options) class SomeClass | ||
* {}` -- it returns a function that *itself* accepts a function (the class constructor) as its | ||
* first argument, and that does the auto-bind decorating by delegating to | ||
* `autoBindMethodsDecorator`. | ||
* | ||
* The delegate method `autoBindMethodsDecorator` is `call`ed in order to avoid changing the context | ||
* from whatever it would ordinarily be in the case of a non-overloaded decorator, while still | ||
* allowing us to pass on any received `options`. | ||
* | ||
* @param {Object|Function|undefined} [input] - optional options or the function/class to decorate | ||
* @param {String[]} [input.methodsToIgnore] - names of methods to skip auto-binding; applicable | ||
* only if `input` is not a function | ||
* @param {boolean} [options.dontOptimize] - if truthy, turns off the decorator's default | ||
* optimization behavior, which is to define the bound method directly on the class instance | ||
* when first accessed, in order to prevent re-binding on every access and traversing the | ||
* prototype chain; applicable only if `input` is not a function | ||
* @returns {Function|undefined} | ||
*/ | ||
function autoBindMethods(input) { | ||
if (typeof input !== 'function') { | ||
return function (target) { | ||
autoBindMethodsDecorator.call(this, target, input); | ||
}; | ||
} | ||
autoBindMethodsDecorator.call(this, input); | ||
} | ||
var prototype = target.prototype; | ||
var _options$methodsToIgn = options.methodsToIgnore, | ||
methodsToIgnore = _options$methodsToIgn === undefined ? [] : _options$methodsToIgn, | ||
_options$dontOptimize = options.dontOptimize, | ||
dontOptimize = _options$dontOptimize === undefined ? false : _options$dontOptimize; | ||
exports.autoBindMethods = autoBindMethods; | ||
var ownProps = typeof Object.getOwnPropertySymbols === 'function' ? Object.getOwnPropertyNames(prototype).concat(Object.getOwnPropertySymbols(prototype)) : Object.getOwnPropertyNames(prototype); | ||
/** | ||
* Convenience decorator that operates the same as above, but that automatically skips all | ||
* methods in the React Component Spec, since they do not need auto-binding on React/Preact | ||
* components. Useful to those using this decorator with React, as there is no need to list | ||
* all of the React Component Spec methods as `methodsToIgnore`. | ||
* | ||
* @param {Object|Function|undefined} [input] - optional options or the function/class to decorate | ||
* @param {String[]} [input.methodsToIgnore] - names of methods to skip auto-binding; applicable | ||
* only if `input` is not a function | ||
* @param {boolean} [options.dontOptimize] - if truthy, turns off the decorator's default | ||
* optimization behavior, which is to define the bound method directly on the class instance | ||
* when first accessed, in order to prevent re-binding on every access and traversing the | ||
* prototype chain; applicable only if `input` is not a function | ||
* @returns {Function|undefined} | ||
*/ | ||
function autoBindMethodsForReact(input) { | ||
if (typeof input === 'undefined') { | ||
return autoBindMethods({ methodsToIgnore: _ReactComponentSpecMethods2.default }); | ||
} | ||
if (methodsToIgnore.length > 0) { | ||
ownProps = ownProps.filter(function (prop) { | ||
return methodsToIgnore.indexOf(prop) === -1; | ||
}); | ||
} | ||
if (typeof input !== 'function') { | ||
if (!(0, _isObject2.default)(input)) { | ||
throw new TypeError('autoBindMethodsForReact was passed an input of type ' + (typeof input === 'undefined' ? 'undefined' : _typeof(input)) + '. The input ' + 'argument must be either a function, a plain JS object, or undefined.'); | ||
} | ||
ownProps.forEach(function (ownPropIdentifier) { | ||
var propDescriptor = Object.getOwnPropertyDescriptor(prototype, ownPropIdentifier); | ||
var value = propDescriptor.value, | ||
configurable = propDescriptor.configurable; | ||
return autoBindMethods(_extends({}, input, { | ||
methodsToIgnore: (0, _uniqueConcatArrays2.default)(input.methodsToIgnore || [], _ReactComponentSpecMethods2.default) | ||
})); | ||
} | ||
return autoBindMethods({ methodsToIgnore: _ReactComponentSpecMethods2.default })(input); | ||
}; | ||
if (typeof value !== 'function' || !configurable) { | ||
// We can only do our work with configurable functions, so bail early here. | ||
return; | ||
/** | ||
* A "legacy"-style "class" decorator function for auto-binding the methods of the "class." | ||
* | ||
* @param {Function} target - an ES2015 "class" or -- what is effectively the same thing -- a | ||
* constructor function. | ||
* @param {Object} [options] - optional options | ||
* @param {string[]} [options.methodsToIgnore] - names of methods to skip auto-binding | ||
* @param {boolean} [options.dontOptimize] - if truthy, turns off the decorator's default | ||
* optimization behavior, which is to define the bound method directly on the class instance | ||
* in order to prevent lookups and re-binding on every access | ||
*/ | ||
function autoBindMethodsDecorator(target) { | ||
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
if (typeof target !== 'function') { | ||
throw new TypeError('The autoBindMethods decorator must be passed a function as the first argument. ' + ('It received an argument of type ' + (typeof target === 'undefined' ? 'undefined' : _typeof(target)) + '.')); | ||
} | ||
Object.defineProperty(prototype, ownPropIdentifier, { | ||
get: function get() { | ||
if (this.hasOwnProperty(ownPropIdentifier)) { | ||
// Don't bind the prototype's method to the prototype, or we can't re-bind it. | ||
return value; | ||
} | ||
var prototype = target.prototype; | ||
var _options$methodsToIgn = options.methodsToIgnore, | ||
methodsToIgnore = _options$methodsToIgn === undefined ? [] : _options$methodsToIgn, | ||
_options$dontOptimize = options.dontOptimize, | ||
dontOptimize = _options$dontOptimize === undefined ? false : _options$dontOptimize; | ||
var boundMethod = value.bind(this); | ||
if (!dontOptimize) { | ||
var writable = propDescriptor.writable, | ||
enumerable = propDescriptor.enumerable; // use same values as prototype for consistency | ||
var ownProps = typeof Object.getOwnPropertySymbols === 'function' ? Object.getOwnPropertyNames(prototype).concat(Object.getOwnPropertySymbols(prototype)) : Object.getOwnPropertyNames(prototype); | ||
// `defineProperty` must be used here rather than a standard assignment because | ||
// assignments will first check for getters/setters up the prototype chain and | ||
// thus reject the assignment (since the property on the prototype has a getter | ||
// but no setter (see: http://www.2ality.com/2012/08/property-definition-assignment.html)) | ||
if (methodsToIgnore.length > 0) { | ||
ownProps = ownProps.filter(function (prop) { | ||
return methodsToIgnore.indexOf(prop) === -1; | ||
}); | ||
} | ||
Object.defineProperty(this, ownPropIdentifier, { | ||
value: boundMethod, | ||
ownProps.forEach(function (ownPropIdentifier) { | ||
if (ownPropIdentifier === 'constructor') { | ||
// This decorator should not muck around with constructors, for fear of introducing | ||
// unexpected side effects. | ||
return; | ||
} | ||
var propDescriptor = Object.getOwnPropertyDescriptor(prototype, ownPropIdentifier); | ||
var value = propDescriptor.value, | ||
configurable = propDescriptor.configurable, | ||
enumerable = propDescriptor.enumerable; | ||
if (typeof value !== 'function' || !configurable) { | ||
// We can only do our work with configurable functions, so bail early here. | ||
return; | ||
} | ||
Object.defineProperty(prototype, ownPropIdentifier, { | ||
// Keep the same enumerability/configurability settings. | ||
enumerable: enumerable, | ||
configurable: configurable, | ||
get: function get() { | ||
if (this.hasOwnProperty(ownPropIdentifier)) { | ||
// Don't bind the prototype's method to the prototype, or we can't re-bind it to instances. | ||
return value; | ||
} | ||
var boundMethod = value.bind(this); | ||
if (!dontOptimize) { | ||
// `defineProperty` must be used here rather than a standard assignment because | ||
// assignments will first check for getters/setters up the prototype chain and | ||
// thus reject the assignment (since the property on the prototype has a getter | ||
// but no setter (see: http://www.2ality.com/2012/08/property-definition-assignment.html)) | ||
Object.defineProperty(this, ownPropIdentifier, { | ||
enumerable: enumerable, | ||
configurable: configurable, | ||
value: boundMethod, | ||
writable: propDescriptor.writable !== false ? true : false | ||
}); | ||
} | ||
return boundMethod; | ||
}, | ||
set: function set(newValue) { | ||
if (propDescriptor.writable === false) { | ||
// If the original property wasn't writable, don't change that. | ||
return; | ||
} | ||
// Re-assigning a property on the prototype *after* the property has been bound by | ||
// the decorator should simply overwrite that property entirely; it is weird (IMO) | ||
// for it to magically be auto-bound to instances when assigned. | ||
Object.defineProperty(prototype, ownPropIdentifier, { | ||
value: newValue, | ||
configurable: true, | ||
writable: writable, | ||
enumerable: enumerable | ||
enumerable: true, | ||
writable: true | ||
}); | ||
} | ||
return boundMethod; | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
} | ||
}); |
# CHANGELOG | ||
## v3.0.0 (11/16/2017) | ||
- Ensures that the decorator does not prevent overwriting autobound | ||
methods on a class/prototype (closes #4), for React 16 interop | ||
- Exports `autoBindMethodsForReact` convenience decorator for easier use | ||
with React/Preact classes (see #4) | ||
- Does not attempt to autoBind the `constructor` method, ever (no need) | ||
- Better guarantees that property descriptors for autobound properties | ||
have the same settings with respect to enumerability, writability, etc. | ||
- Built files are now UMD modules | ||
- Additional tests | ||
## v2.3.0 (8/4/2017) | ||
@@ -4,0 +16,0 @@ |
{ | ||
"name": "class-autobind-decorator", | ||
"version": "2.3.0", | ||
"version": "3.0.0", | ||
"description": "A small framework-agnostic utility for auto-binding \"class\" methods to instances (with customization options) using either \"legacy\" decorator syntax or plain old ES5 (without needing ES2015+ polyfills).", | ||
@@ -8,4 +8,4 @@ "main": "build/index.js", | ||
"scripts": { | ||
"build": "./node_modules/.bin/babel src -d build && cp ./src/index.d.ts ./build", | ||
"test": "npm run build && ./node_modules/.bin/mocha --compilers js:babel-core/register -- test" | ||
"build": "rm -rf ./build && ./node_modules/.bin/babel src -d build && cp ./src/index.d.ts ./build", | ||
"test": "./node_modules/.bin/mocha --compilers js:babel-core/register -- test" | ||
}, | ||
@@ -35,2 +35,4 @@ "keywords": [ | ||
"babel-plugin-transform-decorators-legacy": "^1.3.4", | ||
"babel-plugin-transform-es2015-modules-umd": "^6.24.1", | ||
"babel-plugin-transform-object-rest-spread": "^6.26.0", | ||
"babel-polyfill": "^6.13.0", | ||
@@ -37,0 +39,0 @@ "babel-preset-es2015": "^6.14.0", |
@@ -11,3 +11,4 @@ # class-autobind-decorator | ||
- Usable with ES6+ [React](https://facebook.github.io/react/) and [Preact](https://preactjs.com) classes, | ||
but also elsewhere | ||
but also elsewhere (exports a default `autoBindMethods` decorator, as well as an `autoBindMethodsForReact` | ||
convenience decorator that skips autobinding methods from the React Component Spec that do not require binding) | ||
- Built version is fully ES5-compatible, requiring no ES6+ polyfills | ||
@@ -21,2 +22,5 @@ - Supports class methods that have ES6 Symbols as keys | ||
(checks for configurability first!) | ||
- Keeps property descriptors (e.g., writability, enumerability, etc.) consistent between original | ||
properties and the corresponding autobound properties | ||
- Does not "magically" autobind properties set on classes/prototypes *after* the decorator has been applied | ||
- Conforms to [the ES6+ "legacy" decorator pattern](https://babeljs.io/docs/plugins/transform-decorators/), | ||
@@ -64,4 +68,8 @@ and hence is usable as an ES6+ legacy decorator | ||
import autoBindMethods from 'class-autobind-decorator'; | ||
// NOTE: For React classes, you can use this alternative: | ||
// import { autoBindMethodsForReact } from 'class-autobind-decorator'; | ||
@autoBindMethods({ methodsToIgnore: ['unboundMethod', 'render'] }) | ||
// or: | ||
// @autoBindMethodsForReact({ methodsToIgnore: ['unboundMethod'] }) | ||
class MyComponent extends React.Component { | ||
@@ -68,0 +76,0 @@ someComponentMethod() { |
@@ -0,1 +1,5 @@ | ||
import ReactComponentSpecMethods from './lib/ReactComponentSpecMethods'; | ||
import isObject from './lib/isObject'; | ||
import uniqueConcatArrays from './lib/uniqueConcatArrays'; | ||
/** | ||
@@ -8,3 +12,3 @@ * Overloaded function for auto-binding the methods of a class to the class's relevant instance. If | ||
* {}` -- it returns a function that *itself* accepts a function (the class constructor) as its | ||
* first argument and that does the auto-bind decorating by delegating to | ||
* first argument, and that does the auto-bind decorating by delegating to | ||
* `autoBindMethodsDecorator`. | ||
@@ -16,8 +20,10 @@ * | ||
* | ||
* @param {Object} [options] - optional options | ||
* @param {String[]} [options.methodsToIgnore] - names of methods to skip auto-binding | ||
* @param {Object|Function|undefined} [input] - optional options or the function/class to decorate | ||
* @param {String[]} [input.methodsToIgnore] - names of methods to skip auto-binding; applicable | ||
* only if `input` is not a function | ||
* @param {boolean} [options.dontOptimize] - if truthy, turns off the decorator's default | ||
* optimization behavior, which is to define the bound method directly on the class instance | ||
* in order to prevent lookups and re-binding on every access | ||
* @returns {*} | ||
* when first accessed, in order to prevent re-binding on every access and traversing the | ||
* prototype chain; applicable only if `input` is not a function | ||
* @returns {Function|undefined} | ||
*/ | ||
@@ -34,3 +40,42 @@ export default function autoBindMethods(input) { | ||
export { autoBindMethods }; | ||
/** | ||
* Convenience decorator that operates the same as above, but that automatically skips all | ||
* methods in the React Component Spec, since they do not need auto-binding on React/Preact | ||
* components. Useful to those using this decorator with React, as there is no need to list | ||
* all of the React Component Spec methods as `methodsToIgnore`. | ||
* | ||
* @param {Object|Function|undefined} [input] - optional options or the function/class to decorate | ||
* @param {String[]} [input.methodsToIgnore] - names of methods to skip auto-binding; applicable | ||
* only if `input` is not a function | ||
* @param {boolean} [options.dontOptimize] - if truthy, turns off the decorator's default | ||
* optimization behavior, which is to define the bound method directly on the class instance | ||
* when first accessed, in order to prevent re-binding on every access and traversing the | ||
* prototype chain; applicable only if `input` is not a function | ||
* @returns {Function|undefined} | ||
*/ | ||
export function autoBindMethodsForReact(input) { | ||
if (typeof input === 'undefined') { | ||
return autoBindMethods({ methodsToIgnore: ReactComponentSpecMethods }); | ||
} | ||
if (typeof input !== 'function') { | ||
if (!isObject(input)) { | ||
throw new TypeError( | ||
`autoBindMethodsForReact was passed an input of type ${typeof input}. The input ` + | ||
'argument must be either a function, a plain JS object, or undefined.' | ||
); | ||
} | ||
return autoBindMethods({ | ||
...input, | ||
methodsToIgnore: uniqueConcatArrays(input.methodsToIgnore || [], ReactComponentSpecMethods) | ||
}); | ||
} | ||
return autoBindMethods({ methodsToIgnore: ReactComponentSpecMethods })(input); | ||
}; | ||
/** | ||
* A "legacy"-style "class" decorator function for auto-binding the methods of the "class." | ||
@@ -66,4 +111,10 @@ * | ||
ownProps.forEach((ownPropIdentifier) => { | ||
if (ownPropIdentifier === 'constructor') { | ||
// This decorator should not muck around with constructors, for fear of introducing | ||
// unexpected side effects. | ||
return; | ||
} | ||
const propDescriptor = Object.getOwnPropertyDescriptor(prototype, ownPropIdentifier); | ||
const { value, configurable } = propDescriptor; | ||
const { value, configurable, enumerable } = propDescriptor; | ||
@@ -76,5 +127,8 @@ if (typeof value !== 'function' || !configurable) { | ||
Object.defineProperty(prototype, ownPropIdentifier, { | ||
// Keep the same enumerability/configurability settings. | ||
enumerable, | ||
configurable, | ||
get() { | ||
if (this.hasOwnProperty(ownPropIdentifier)) { | ||
// Don't bind the prototype's method to the prototype, or we can't re-bind it. | ||
// Don't bind the prototype's method to the prototype, or we can't re-bind it to instances. | ||
return value; | ||
@@ -86,4 +140,2 @@ } | ||
if (!dontOptimize) { | ||
const { writable, enumerable } = propDescriptor; // use same values as prototype for consistency | ||
// `defineProperty` must be used here rather than a standard assignment because | ||
@@ -94,6 +146,6 @@ // assignments will first check for getters/setters up the prototype chain and | ||
Object.defineProperty(this, ownPropIdentifier, { | ||
enumerable, | ||
configurable, | ||
value: boundMethod, | ||
configurable: true, | ||
writable, | ||
enumerable | ||
writable: propDescriptor.writable !== false ? true : false | ||
}); | ||
@@ -103,2 +155,18 @@ } | ||
return boundMethod; | ||
}, | ||
set(newValue) { | ||
if (propDescriptor.writable === false) { | ||
// If the original property wasn't writable, don't change that. | ||
return; | ||
} | ||
// Re-assigning a property on the prototype *after* the property has been bound by | ||
// the decorator should simply overwrite that property entirely; it is weird (IMO) | ||
// for it to magically be auto-bound to instances when assigned. | ||
Object.defineProperty(prototype, ownPropIdentifier, { | ||
value: newValue, | ||
configurable: true, | ||
enumerable: true, | ||
writable: true | ||
}); | ||
} | ||
@@ -105,0 +173,0 @@ }); |
import 'babel-polyfill'; | ||
import { expect } from 'chai'; | ||
import autoBindMethods from '../build'; | ||
import autoBindMethods, { autoBindMethodsForReact } from '../src'; | ||
@@ -8,12 +8,23 @@ const symbolKey = Symbol('symbolKey'); | ||
/** | ||
* Helper method for performing tests. | ||
* Helper method for performing tests. TODO: This is probably overkill. | ||
* | ||
* @param {Function} MyFirstClass | ||
* @param {Function} MySecondClass | ||
* @param {Function} UnboundClass | ||
* @param {Function} FullyBoundClass | ||
* @param {Array.<string|Symbol>} [ignoredMethods] | ||
*/ | ||
function testBindings(MyFirstClass, MySecondClass, MyThirdClass, ignoredMethods = []) { | ||
const unboundInstance = new MyFirstClass(); | ||
const boundInstance = new MySecondClass(); | ||
const boundConfiguredInstance = new MyThirdClass(); | ||
function testBindings( | ||
{ | ||
UnboundClass, | ||
FullyBoundClass, | ||
PartiallyBoundClass, | ||
FullyBoundReactClass, | ||
PartiallyBoundReactClass | ||
}, | ||
ignoredMethods = [] | ||
) { | ||
const unboundInstance = new UnboundClass(); | ||
const boundInstance = new FullyBoundClass(); | ||
const boundConfiguredInstance = new PartiallyBoundClass(); | ||
const boundReactInstance = new FullyBoundReactClass(); | ||
const boundConfiguredReactInstance = new PartiallyBoundReactClass(); | ||
const unboundTestMethodOne = unboundInstance.testMethodOne; | ||
@@ -26,10 +37,20 @@ const unboundSymbolMethod = unboundInstance[symbolKey]; | ||
const boundConfiguredSymbolMethod = boundConfiguredInstance[symbolKey]; | ||
const boundReactTestMethodOne = boundReactInstance.testMethodOne; | ||
const boundReactSymbolMethod = boundReactInstance[symbolKey]; | ||
const boundConfiguredReactTestMethodOne = boundConfiguredReactInstance.testMethodOne; | ||
const boundConfiguredReactTestMethodTwo = boundConfiguredReactInstance.testMethodTwo; | ||
const boundConfiguredReactSymbolMethod = boundConfiguredReactInstance[symbolKey]; | ||
expect(unboundTestMethodOne(unboundInstance)).to.equal(false); | ||
expect(unboundSymbolMethod(unboundInstance)).to.equal(false); | ||
expect(boundTestMethodOne(boundInstance)).to.equal(true); | ||
expect(boundSymbolMethod(boundInstance)).to.equal(true); | ||
expect(unboundTestMethodOne(unboundInstance)).to.be.false; | ||
expect(unboundSymbolMethod(unboundInstance)).to.be.false; | ||
expect(boundTestMethodOne(boundInstance)).to.be.true; | ||
expect(boundSymbolMethod(boundInstance)).to.be.true; | ||
expect(boundConfiguredTestMethodOne(boundConfiguredInstance)).to.equal(ignoredMethods.includes('testMethodOne') ? false : true); | ||
expect(boundConfiguredTestMethodTwo(boundConfiguredInstance)).to.equal(ignoredMethods.includes('testMethodTwo') ? false : true); | ||
expect(boundConfiguredSymbolMethod(boundConfiguredInstance)).to.equal(ignoredMethods.includes(symbolKey) ? false : true); | ||
expect(boundReactTestMethodOne(boundReactInstance)).to.be.true; | ||
expect(boundReactSymbolMethod(boundReactInstance)).to.be.true; | ||
expect(boundConfiguredReactTestMethodOne(boundConfiguredReactInstance)).to.equal(ignoredMethods.includes('testMethodOne') ? false : true); | ||
expect(boundConfiguredReactTestMethodTwo(boundConfiguredReactInstance)).to.equal(ignoredMethods.includes('testMethodTwo') ? false : true); | ||
expect(boundConfiguredReactSymbolMethod(boundConfiguredReactInstance)).to.equal(ignoredMethods.includes(symbolKey) ? false : true); | ||
} | ||
@@ -42,7 +63,13 @@ | ||
/** | ||
* Retrieves three classes for testing -- one unbound, one auto-bound, and one | ||
* auto-bound-with-options (some methods ignored). | ||
* Retrieves five classes for testing -- one unbound, one auto-bound, one | ||
* auto-bound-with-options (some methods ignored), and one auto-bound-for-React, | ||
* and one auto-bound-for-React-with-options (some additional methods ignored). | ||
* The classes are defined inline so that new ones are returned for each test | ||
* that uses this method. | ||
* | ||
* TODO: This also seems to be overkill; does far more than is necessary for | ||
* most tests below. | ||
*/ | ||
function getClasses(autoBindOptions) { | ||
class MyFirstClass { | ||
class UnboundClass { | ||
testMethodOne(instance) { | ||
@@ -58,3 +85,3 @@ return isBound(instance, this); | ||
@autoBindMethods | ||
class MySecondClass { | ||
class FullyBoundClass { | ||
testMethodOne(instance) { | ||
@@ -70,3 +97,3 @@ return isBound(instance, this); | ||
@autoBindMethods(autoBindOptions) | ||
class MyThirdClass { | ||
class PartiallyBoundClass { | ||
testMethodOne(instance) { | ||
@@ -85,3 +112,39 @@ return isBound(instance, this); | ||
return [MyFirstClass, MySecondClass, MyThirdClass]; | ||
@autoBindMethodsForReact | ||
class FullyBoundReactClass { | ||
testMethodOne(instance) { | ||
return isBound(instance, this); | ||
} | ||
[symbolKey](instance) { | ||
return isBound(instance, this); | ||
} | ||
render() {} | ||
} | ||
@autoBindMethodsForReact(autoBindOptions) | ||
class PartiallyBoundReactClass { | ||
testMethodOne(instance) { | ||
return isBound(instance, this); | ||
} | ||
testMethodTwo(instance) { | ||
return isBound(instance, this); | ||
} | ||
[symbolKey](instance) { | ||
return isBound(instance, this); | ||
} | ||
render() {} | ||
} | ||
return { | ||
UnboundClass, | ||
FullyBoundClass, | ||
PartiallyBoundClass, | ||
FullyBoundReactClass, | ||
PartiallyBoundReactClass | ||
}; | ||
} | ||
@@ -105,5 +168,103 @@ | ||
it('should not redefine constructors', function () { | ||
const { UnboundClass } = getClasses(); | ||
const firstInstance = new UnboundClass(); | ||
const untouchedConstructor = UnboundClass.prototype.constructor; | ||
autoBindMethods(UnboundClass); | ||
const secondInstance = new UnboundClass(); | ||
expect(firstInstance.constructor).to.equal(UnboundClass.prototype.constructor); | ||
expect(secondInstance.constructor).to.equal(UnboundClass.prototype.constructor); | ||
expect(UnboundClass.prototype.constructor).to.equal(untouchedConstructor); | ||
}); | ||
// This test turns out to be important for React 16 interop. | ||
// See https://github.com/jmrog/class-autobind-decorator/issues/4 | ||
it('should allow overwriting autobound properties', function () { | ||
const { FullyBoundClass } = getClasses(); | ||
const instance = new FullyBoundClass(); | ||
instance.testMethodOne(); // autobind with optimization to write property on instance | ||
function test() { | ||
instance.testMethodOne = 're-assigned'; | ||
FullyBoundClass.prototype.constructor = 'nope'; | ||
FullyBoundClass.prototype.testMethodOne = function () { return 'x'; }; | ||
return true; | ||
} | ||
expect(test).not.to.throw; | ||
test(); // ensure this is run before the next two tests; the prior `expect` line does not do that (there's probably a better way to do this) | ||
expect(FullyBoundClass.prototype.testMethodOne()).to.equal('x'); | ||
expect(instance.testMethodOne).to.equal('re-assigned'); | ||
}); | ||
it('should maintain property descriptor behavior as much as possible', function () { | ||
const { UnboundClass } = getClasses(); | ||
function testEnumeration(testee) { | ||
for (let item in testee) { | ||
if (item === 'specialProperty') { | ||
throw new Error(); | ||
} | ||
} | ||
} | ||
function testWritability(testee) { | ||
testee.specialProperty = false; | ||
} | ||
Object.defineProperty(UnboundClass.prototype, 'specialProperty', { | ||
value: () => 'unwritable and unenumerable!', | ||
configurable: true | ||
}); | ||
autoBindMethods(UnboundClass); | ||
const instance = new UnboundClass(); | ||
expect(() => testEnumeration(instance)).not.to.throw; | ||
expect(() => testWritability(instance)).not.to.throw; | ||
expect(instance.specialProperty()).to.equal('unwritable and unenumerable!'); | ||
expect(() => testEnumeration(UnboundClass.prototype)).not.to.throw; | ||
expect(() => testWritability(UnboundClass.prototype)).to.throw; | ||
expect(UnboundClass.prototype.specialProperty()).to.equal('unwritable and unenumerable!'); | ||
}); | ||
describe('overwriting an autobound property', function () { | ||
it('should behave like normal assignment (descriptor-wise, plus no magic autobinding)', function () { | ||
const { FullyBoundClass } = getClasses(); | ||
const instance = new FullyBoundClass(); | ||
FullyBoundClass.prototype.testMethodOne = function() { | ||
return this === FullyBoundClass.prototype; | ||
}; | ||
instance.testMethodOne(); // trigger binding to instance | ||
instance.testMethodOne = function() { | ||
return this === instance; | ||
}; | ||
const prototypeDescriptor = Object.getOwnPropertyDescriptor(FullyBoundClass.prototype, 'testMethodOne'); | ||
const instanceDescriptor = Object.getOwnPropertyDescriptor(instance, 'testMethodOne'); | ||
const prototypeMethod = FullyBoundClass.prototype.testMethodOne; | ||
const instanceMethod = instance.testMethodOne; | ||
const newInstance = new FullyBoundClass(); | ||
newInstance.testMethodOne(); // should not magically autobind | ||
const newInstanceMethod = newInstance.testMethodOne; | ||
[prototypeDescriptor, instanceDescriptor].forEach((descriptor) => { | ||
['configurable', 'enumerable', 'writable'].forEach((setting) => { | ||
expect(descriptor[setting]).to.be.true; | ||
}); | ||
}); | ||
expect(prototypeMethod()).to.be.false; | ||
expect(instanceMethod()).to.be.false; | ||
expect(newInstanceMethod()).to.be.false; | ||
}); | ||
}); | ||
describe('when returned from a call that specifies no options', function () { | ||
it('should autobind all methods', function () { | ||
testBindings(...getClasses()); | ||
testBindings(getClasses()); | ||
}); | ||
@@ -115,3 +276,3 @@ }); | ||
it('should bind only the unignored methods', function () { | ||
testBindings(...getClasses(autoBindOptions), autoBindOptions.methodsToIgnore); | ||
testBindings(getClasses(autoBindOptions), autoBindOptions.methodsToIgnore); | ||
}); | ||
@@ -122,7 +283,7 @@ }); | ||
it('should not alter the instance and should re-bind on every access', function () { | ||
const [MyFirstClass] = getClasses(); | ||
const { UnboundClass } = getClasses(); | ||
const customAutoBinder = autoBindMethods({ dontOptimize: true }); | ||
customAutoBinder(MyFirstClass); | ||
const myFirstInstance = new MyFirstClass(); | ||
customAutoBinder(UnboundClass); | ||
const myFirstInstance = new UnboundClass(); | ||
@@ -132,4 +293,4 @@ const a = myFirstInstance.testMethodOne; | ||
expect(a).not.to.equal(b); | ||
expect(myFirstInstance.hasOwnProperty('testMethodOne')).to.equal(false); | ||
expect(b(myFirstInstance)).to.equal(true); | ||
expect(myFirstInstance.hasOwnProperty('testMethodOne')).to.be.false; | ||
expect(b(myFirstInstance)).to.be.true; | ||
}); | ||
@@ -140,4 +301,4 @@ }); | ||
it('should alter the instance and not re-bind on every access', function () { | ||
const [, MySecondClass] = getClasses(); | ||
const myFirstInstance = new MySecondClass(); | ||
const { FullyBoundClass } = getClasses(); | ||
const myFirstInstance = new FullyBoundClass(); | ||
@@ -147,3 +308,3 @@ const a = myFirstInstance.testMethodOne; | ||
expect(a).to.equal(b); | ||
expect(myFirstInstance.hasOwnProperty('testMethodOne')).to.equal(true); | ||
expect(myFirstInstance.hasOwnProperty('testMethodOne')).to.be.true; | ||
}); | ||
@@ -155,5 +316,5 @@ }); | ||
it('should skip binding those functions and not throw', function () { | ||
const [MyFirstClass] = getClasses(); | ||
const { UnboundClass } = getClasses(); | ||
Object.defineProperty(MyFirstClass.prototype, 'nonConfigurable', { | ||
Object.defineProperty(UnboundClass.prototype, 'nonConfigurable', { | ||
value: function (instance) { | ||
@@ -165,9 +326,10 @@ return instance === this; | ||
expect(autoBindMethods(MyFirstClass)).not.to.throw; | ||
expect(() => autoBindMethods(UnboundClass)).not.to.throw; | ||
const myInstance = new MyFirstClass(); | ||
autoBindMethods(UnboundClass); // ensure this happens before next lines; prior `expect` line doesn't do this (there's probably a better way to do this) | ||
const myInstance = new UnboundClass(); | ||
const { testMethodOne, nonConfigurable } = myInstance; | ||
expect(testMethodOne(myInstance)).to.equal(true); | ||
expect(nonConfigurable(myInstance)).to.equal(false); | ||
expect(testMethodOne(myInstance)).to.be.true; | ||
expect(nonConfigurable(myInstance)).to.be.false; | ||
}); | ||
@@ -178,10 +340,10 @@ }); | ||
it('should not bind the method to the prototype', function () { | ||
const [ , MySecondClass] = getClasses(); | ||
MySecondClass.prototype.testMethodOne; // `get` via prototype, not instance, should not bind | ||
const { FullyBoundClass } = getClasses(); | ||
FullyBoundClass.prototype.testMethodOne; // `get` via prototype, not instance, should not bind | ||
const myInstance = new MySecondClass(); | ||
const myInstance = new FullyBoundClass(); | ||
myInstance.__proto__.testMethodOne; // similarly, should not bind | ||
const { testMethodOne } = myInstance; | ||
expect(testMethodOne(myInstance)).to.equal(true); // should be bound to the instance now, not the prototype | ||
expect(testMethodOne(myInstance)).to.be.true; // should be bound to the instance now, not the prototype | ||
}); | ||
@@ -192,20 +354,46 @@ }); | ||
it('should bind the method to each individual instance', function () { | ||
const [ , MySecondClass] = getClasses(); | ||
const myFirstInstance = new MySecondClass(); | ||
const mySecondInstance = new MySecondClass(); | ||
const { FullyBoundClass } = getClasses(); | ||
const myFirstInstance = new FullyBoundClass(); | ||
const mySecondInstance = new FullyBoundClass(); | ||
let { testMethodOne } = myFirstInstance; | ||
expect(testMethodOne(myFirstInstance)).to.equal(true); | ||
expect(testMethodOne(mySecondInstance)).to.equal(false); | ||
expect(myFirstInstance.hasOwnProperty('testMethodOne')).to.equal(true); | ||
expect(MySecondClass.prototype.hasOwnProperty('testMethodOne')).to.equal(true); | ||
expect(mySecondInstance.hasOwnProperty('testMethodOne')).to.equal(false); | ||
expect(testMethodOne(myFirstInstance)).to.be.true; | ||
expect(testMethodOne(mySecondInstance)).to.be.false; | ||
expect(myFirstInstance.hasOwnProperty('testMethodOne')).to.be.true; | ||
expect(FullyBoundClass.prototype.hasOwnProperty('testMethodOne')).to.be.true; | ||
expect(mySecondInstance.hasOwnProperty('testMethodOne')).to.be.false; | ||
testMethodOne = mySecondInstance.testMethodOne; | ||
expect(testMethodOne(mySecondInstance)).to.equal(true); | ||
expect(mySecondInstance.hasOwnProperty('testMethodOne')).to.equal(true); | ||
expect(testMethodOne(mySecondInstance)).to.be.true; | ||
expect(mySecondInstance.hasOwnProperty('testMethodOne')).to.be.true; | ||
}); | ||
}); | ||
}); | ||
describe('autoBindMethodsForReact', function () { | ||
it('should throw if input is neither a function nor a plain object nor falsey', function () { | ||
expect(() => autoBindMethodsForReact()).not.to.throw; | ||
expect(() => autoBindMethodsForReact({})).not.to.throw; | ||
expect(() => autoBindMethodsForReact(function () {})).not.to.throw; | ||
expect(() => autoBindMethodsForReact([])).to.throw; | ||
expect(() => autoBindMethodsForReact(null)).to.throw; | ||
expect(() => autoBindMethodsForReact(false)).to.throw; | ||
expect(() => autoBindMethodsForReact(Symbol())).to.throw; | ||
}); | ||
it('should skip binding methods in the React Component Spec to instances', function () { | ||
@autoBindMethodsForReact | ||
class TestClass { | ||
render() {} | ||
} | ||
const testInstance = new TestClass(); | ||
testInstance.render(); | ||
// the method would be on the instance if the decorator had applied | ||
expect('render' in testInstance).to.be.true; | ||
expect(testInstance.hasOwnProperty('render')).to.be.false; | ||
}); | ||
}); |
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
51566
17
864
191
9
1