├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── src ├── decko.d.ts └── decko.js ├── tests ├── bind.js ├── debounce.js ├── index.ts └── memoize.js └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "modules": "umd", 3 | "loose": "all", 4 | "compact": true, 5 | "comments": false, 6 | "stage": 0 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "env": { 5 | "browser": true 6 | }, 7 | "ecmaFeatures": { 8 | "modules": true, 9 | "jsx": true 10 | }, 11 | "rules": { 12 | "no-unused-vars": [1, { "args": "after-used" }], 13 | "no-cond-assign": 1, 14 | "semi": 2, 15 | "camelcase": 0, 16 | "comma-style": 2, 17 | "comma-dangle": [2, "never"], 18 | "indent": [2, "tab", {"SwitchCase": 1}], 19 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"], 20 | "no-trailing-spaces": [2, { "skipBlankLines": true }], 21 | "max-nested-callbacks": [2, 3], 22 | "no-eval": 2, 23 | "no-implied-eval": 2, 24 | "no-new-func": 2, 25 | "guard-for-in": 2, 26 | "eqeqeq": 2, 27 | "no-else-return": 2, 28 | "no-redeclare": 2, 29 | "no-dupe-keys": 2, 30 | "radix": 2, 31 | "strict": [2, "never"], 32 | "no-shadow": 0, 33 | "callback-return": [1, ["callback", "cb", "next", "done"]], 34 | "no-delete-var": 2, 35 | "no-undef-init": 2, 36 | "no-shadow-restricted-names": 2, 37 | "handle-callback-err": 0, 38 | "no-lonely-if": 2, 39 | "space-return-throw-case": 2, 40 | "constructor-super": 2, 41 | "no-this-before-super": 2, 42 | "no-dupe-class-members": 2, 43 | "no-const-assign": 2, 44 | "prefer-spread": 2, 45 | "no-useless-concat": 2, 46 | "no-var": 2, 47 | "object-shorthand": 2, 48 | "prefer-arrow-callback": 2 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | dist 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.2.1 2 | * Don't cause infinite recursion when `@bind` decorator is used in IE11 [#8](https://github.com/developit/decko/issues/8) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jason Miller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # decko [![NPM Version](https://img.shields.io/npm/v/decko.svg?style=flat)](https://npmjs.com/package/decko) [![Build Status](https://travis-ci.org/developit/decko.svg?branch=master)](https://travis-ci.org/developit/decko) 2 | 3 | A concise implementation of the three most useful [decorators](https://github.com/wycats/javascript-decorators): 4 | 5 | - `@bind`: make the value of `this` constant within a method 6 | - `@debounce`: throttle calls to a method 7 | - `@memoize`: cache return values based on arguments 8 | 9 | Decorators help simplify code by replacing the noise of common patterns with declarative annotations. 10 | Conversely, decorators can also be overused and create obscurity. 11 | Decko establishes 3 standard decorators that are immediately recognizable, so you can avoid creating decorators in your own codebase. 12 | 13 | > 💡 **Tip:** decko is particularly well-suited to [**Preact Classful Components**](https://github.com/developit/preact). 14 | > 15 | > 💫 **Note:** 16 | > - For Babel 6+, be sure to install [babel-plugin-transform-decorators-legacy](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy). 17 | > - For Typescript, be sure to enable `{"experimentalDecorators": true}` in your tsconfig.json. 18 | 19 | ## Installation 20 | 21 | Available on [npm](https://npmjs.com/package/decko): 22 | 23 | ```sh 24 | npm i -S decko 25 | ``` 26 | 27 | 28 | ## Usage 29 | 30 | Each decorator method is available as a named import. 31 | 32 | ```js 33 | import { bind, memoize, debounce } from 'decko'; 34 | ``` 35 | 36 | 37 | ### `@bind` 38 | 39 | ```js 40 | class Example { 41 | @bind 42 | foo() { 43 | // the value of `this` is always the object from which foo() was referenced. 44 | return this; 45 | } 46 | } 47 | 48 | let e = new Example(); 49 | assert.equal(e.foo.call(null), e); 50 | ``` 51 | 52 | 53 | 54 | ### `@memoize` 55 | 56 | > Cache values returned from the decorated function. 57 | > Uses the first argument as a cache key. 58 | > _Cache keys are always converted to strings._ 59 | > 60 | > ##### Options: 61 | > 62 | > `caseSensitive: false` - _Makes cache keys case-insensitive_ 63 | > 64 | > `cache: {}` - _Presupply cache storage, for seeding or sharing entries_ 65 | 66 | ```js 67 | class Example { 68 | @memoize 69 | expensive(key) { 70 | let start = Date.now(); 71 | while (Date.now()-start < 500) key++; 72 | return key; 73 | } 74 | } 75 | 76 | let e = new Example(); 77 | 78 | // this takes 500ms 79 | let one = e.expensive(1); 80 | 81 | // this takes 0ms 82 | let two = e.expensive(1); 83 | 84 | // this takes 500ms 85 | let three = e.expensive(2); 86 | ``` 87 | 88 | 89 | 90 | ### `@debounce` 91 | 92 | > Throttle calls to the decorated function. To debounce means "call this at most once per N ms". 93 | > All outward function calls get collated into a single inward call, and only the latest (most recent) arguments as passed on to the debounced function. 94 | > 95 | > ##### Options: 96 | > 97 | > `delay: 0` - _The number of milliseconds to buffer calls for._ 98 | 99 | ```js 100 | class Example { 101 | @debounce 102 | foo() { 103 | return this; 104 | } 105 | } 106 | 107 | let e = new Example(); 108 | 109 | // this will only call foo() once: 110 | for (let i=1000; i--) e.foo(); 111 | ``` 112 | 113 | 114 | --- 115 | 116 | License 117 | ------- 118 | 119 | MIT 120 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "decko", 3 | "version": "1.2.1", 4 | "main": "dist/decko.js", 5 | "types": "dist/decko.d.ts", 6 | "description": "A collection of the most useful property decorators.", 7 | "scripts": { 8 | "build": "mkdir -p dist && babel -f src/decko.js -s -o $npm_package_main < src/decko.js && npm run build:ts", 9 | "build:ts": "cp src/decko.d.ts dist/", 10 | "test": "npm run test:ts && eslint {src,tests}/**.js && mocha --compilers js:babel/register tests/**/*.js", 11 | "test:ts": "tsc -p ./", 12 | "style:ts": "tsfmt -r", 13 | "prepublish": "npm run build", 14 | "release": "npm run build && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" 15 | }, 16 | "files": [ 17 | "src", 18 | "dist" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "git://github.com/developit/decko.git" 23 | }, 24 | "devDependencies": { 25 | "babel": "^5.8.21", 26 | "babel-eslint": "^4.1.6", 27 | "chai": "^3.2.0", 28 | "eslint": "^1.10.3", 29 | "mocha": "^2.3.0", 30 | "typescript": "2.1.6", 31 | "typescript-formatter": "4.1.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/decko.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | */ 4 | export function bind( 5 | target: Object, 6 | propertyKey: string | symbol, 7 | descriptor?: TypedPropertyDescriptor 8 | ): TypedPropertyDescriptor | void; 9 | export function bind(): MethodDecorator; 10 | 11 | /** 12 | * @param caseSensitive Makes cache keys case-insensitive 13 | * @param cache Presupply cache storage, for seeding or sharing entries 14 | */ 15 | 16 | export function memoize( 17 | target: Object, 18 | propertyKey: string | symbol, 19 | descriptor?: TypedPropertyDescriptor 20 | ): TypedPropertyDescriptor | void; 21 | export function memoize(caseSensitive?: boolean, cache?: Object): MethodDecorator; 22 | /** 23 | * @param delay number 24 | */ 25 | export function debounce( 26 | target: Object, 27 | propertyKey: string | symbol, 28 | descriptor?: TypedPropertyDescriptor 29 | ): TypedPropertyDescriptor | void; 30 | export function debounce(delay?: number): MethodDecorator; -------------------------------------------------------------------------------- /src/decko.js: -------------------------------------------------------------------------------- 1 | 2 | const EMPTY = {}; 3 | const HOP = Object.prototype.hasOwnProperty; 4 | 5 | let fns = { 6 | /** let cachedFn = memoize(originalFn); */ 7 | memoize(fn, opt=EMPTY) { 8 | let cache = opt.cache || {}; 9 | return function(...a) { 10 | let k = String(a[0]); 11 | if (opt.caseSensitive===false) k = k.toLowerCase(); 12 | return HOP.call(cache,k) ? cache[k] : (cache[k] = fn.apply(this, a)); 13 | }; 14 | }, 15 | 16 | /** let throttled = debounce(10, console.log); */ 17 | debounce(fn, opts) { 18 | if (typeof opts==='function') { let p = fn; fn = opts; opts = p; } 19 | let delay = opts && opts.delay || opts || 0, 20 | args, context, timer; 21 | return function(...a) { 22 | args = a; 23 | context = this; 24 | if (!timer) timer = setTimeout( () => { 25 | fn.apply(context, args); 26 | args = context = timer = null; 27 | }, delay); 28 | }; 29 | }, 30 | 31 | bind(target, key, { value: fn }) { 32 | // In IE11 calling Object.defineProperty has a side-effect of evaluating the 33 | // getter for the property which is being replaced. This causes infinite 34 | // recursion and an "Out of stack space" error. 35 | let definingProperty = false; 36 | return { 37 | configurable: true, 38 | get() { 39 | if (definingProperty) { 40 | return fn; 41 | } 42 | let value = fn.bind(this); 43 | definingProperty = true; 44 | Object.defineProperty(this, key, { 45 | value, 46 | configurable: true, 47 | writable: true 48 | }); 49 | definingProperty = false; 50 | return value; 51 | } 52 | }; 53 | } 54 | }; 55 | 56 | 57 | let memoize = multiMethod(fns.memoize), 58 | debounce = multiMethod(fns.debounce), 59 | bind = multiMethod((f,c)=>f.bind(c), ()=>fns.bind); 60 | 61 | export { memoize, debounce, bind }; 62 | export default { memoize, debounce, bind }; 63 | 64 | 65 | /** Creates a function that supports the following calling styles: 66 | * d() - returns an unconfigured decorator 67 | * d(opts) - returns a configured decorator 68 | * d(fn, opts) - returns a decorated proxy to `fn` 69 | * d(target, key, desc) - the decorator itself 70 | * 71 | * @Example: 72 | * // simple identity deco: 73 | * let d = multiMethod( fn => fn ); 74 | * 75 | * class Foo { 76 | * @d 77 | * bar() { } 78 | * 79 | * @d() 80 | * baz() { } 81 | * 82 | * @d({ opts }) 83 | * bat() { } 84 | * 85 | * bap = d(() => {}) 86 | * } 87 | */ 88 | function multiMethod(inner, deco) { 89 | deco = deco || inner.decorate || decorator(inner); 90 | let d = deco(); 91 | return (...args) => { 92 | let l = args.length; 93 | return (l<2 ? deco : (l>2 ? d : inner))(...args); 94 | }; 95 | } 96 | 97 | /** Returns function supports the forms: 98 | * deco(target, key, desc) -> decorate a method 99 | * deco(Fn) -> call the decorator proxy on a function 100 | */ 101 | function decorator(fn) { 102 | return opt => ( 103 | typeof opt==='function' ? fn(opt) : (target, key, desc) => { 104 | desc.value = fn(desc.value, opt, target, key, desc); 105 | } 106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /tests/bind.js: -------------------------------------------------------------------------------- 1 | import { bind } from '..'; 2 | import { expect } from 'chai'; 3 | 4 | /*global describe,it*/ 5 | 6 | describe('bind()', () => { 7 | it('should bind when used as a simple decorator', next => { 8 | let c = { 9 | @bind 10 | foo() { 11 | return this; 12 | } 13 | }; 14 | 15 | expect(c.foo()).to.equal(c); 16 | 17 | let p = c.foo; 18 | expect(p()).to.equal(c); 19 | 20 | let a = {}; 21 | expect(c.foo.call(a)).to.equal(c); 22 | 23 | next(); 24 | }); 25 | 26 | it('should bind when used as a function', next => { 27 | let ctx = {}, 28 | c = bind(function(){ return this; }, ctx); 29 | 30 | expect(c()).to.equal(ctx); 31 | 32 | let a = {}; 33 | expect(c.call(a)).to.equal(ctx); 34 | 35 | next(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/debounce.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '..'; 2 | import { expect } from 'chai'; 3 | 4 | /*global describe,it*/ 5 | 6 | describe('debounce()', () => { 7 | it('should debounce when used as a simple decorator', next => { 8 | let c = { 9 | calls: 0, 10 | args: null, 11 | 12 | @debounce 13 | foo(...args) { 14 | c.calls++; 15 | c.args = args; 16 | c.context = this; 17 | } 18 | }; 19 | 20 | expect(c).to.have.property('calls', 0); 21 | c.foo(1); 22 | expect(c).to.have.property('calls', 0); 23 | c.foo(2); 24 | c.foo(3); 25 | setTimeout( () => { 26 | expect(c).to.have.property('calls', 1); 27 | expect(c.args).to.deep.equal([3]); 28 | expect(c.context).to.equal(c); 29 | 30 | next(); 31 | }, 20); 32 | }); 33 | 34 | it('should debounce when used as a function', next => { 35 | let c = debounce( (...args) => { 36 | m.calls++; 37 | m.args = args; 38 | }), 39 | m = { calls:0, args:null }; 40 | 41 | expect(m).to.have.property('calls', 0); 42 | c(1); 43 | expect(m).to.have.property('calls', 0); 44 | c(2); 45 | c(3); 46 | setTimeout( () => { 47 | expect(m).to.have.property('calls', 1); 48 | expect(m.args).to.deep.equal([3]); 49 | 50 | next(); 51 | }, 20); 52 | }); 53 | 54 | it('should support passing a delay', next => { 55 | let c = debounce(5, (...args) => { 56 | m.calls.push(args); 57 | }), 58 | m = { calls:[] }; 59 | 60 | c(1); 61 | setTimeout(()=> c(2), 1); 62 | setTimeout(()=> c(3), 10); 63 | setTimeout(()=> c(4), 14); 64 | setTimeout(()=> c(5), 22); 65 | expect(m.calls).to.have.length(0); 66 | setTimeout( () => { 67 | expect(m.calls).to.deep.equal([ [2], [4], [5] ]); 68 | next(); 69 | }, 30); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/index.ts: -------------------------------------------------------------------------------- 1 | import { bind, debounce, memoize } from '..'; 2 | class C { 3 | @bind 4 | foo() { } 5 | 6 | @debounce 7 | moo() { } 8 | 9 | @debounce(1000) 10 | mooWithCustomDelay() { } 11 | 12 | @memoize 13 | mem() { } 14 | 15 | @memoize(true) 16 | memWithConfig() { } 17 | } -------------------------------------------------------------------------------- /tests/memoize.js: -------------------------------------------------------------------------------- 1 | import { memoize } from '..'; 2 | import { expect } from 'chai'; 3 | 4 | /*global describe,it*/ 5 | 6 | describe('memoize()', () => { 7 | it('should memoize when used as a simple decorator', next => { 8 | let c = { 9 | @memoize 10 | foo(key) { 11 | c[key] = (c[key] || 0) + 1; 12 | } 13 | }; 14 | 15 | expect(c).not.to.have.property('a'); 16 | c.foo('a'); 17 | expect(c).to.have.property('a', 1); 18 | c.foo('a'); 19 | c.foo('a'); 20 | expect(c).to.have.property('a', 1); 21 | 22 | next(); 23 | }); 24 | 25 | it('should memoize when used as a function', next => { 26 | let c = memoize( key => { 27 | m[key] = (m[key] || 0) + 1; 28 | }), 29 | m = {}; 30 | 31 | expect(m).not.to.have.property('a'); 32 | c('a'); 33 | expect(m).to.have.property('a', 1); 34 | c('a'); 35 | c('a'); 36 | expect(m).to.have.property('a', 1); 37 | 38 | next(); 39 | }); 40 | 41 | it('should memoize when called without arguments', next => { 42 | let c = memoize( key => { 43 | m[key] = (m[key] || 0) + 1; 44 | }), 45 | m = {}; 46 | 47 | expect(m).not.to.have.property('undefined'); 48 | c(); 49 | expect(m).to.have.property('undefined', 1); 50 | c(); 51 | c(); 52 | expect(m).to.have.property('undefined', 1); 53 | 54 | next(); 55 | }); 56 | 57 | it('should memoize when called with an empty string', next => { 58 | let c = memoize( key => { 59 | m[key] = (m[key] || 0) + 1; 60 | }), 61 | m = {}; 62 | 63 | expect(m).not.to.have.property(''); 64 | c(''); 65 | expect(m).to.have.property('', 1); 66 | c(''); 67 | c(''); 68 | expect(m).to.have.property('', 1); 69 | 70 | next(); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "target": "es2016", 5 | "lib": [ 6 | "dom", 7 | "es2016" 8 | ], 9 | "baseUrl": "./", 10 | "noImplicitAny": true, 11 | "experimentalDecorators": true, 12 | "sourceMap": false, 13 | "moduleResolution": "node", 14 | "strictNullChecks": true, 15 | "declaration": true, 16 | "noEmit": true, 17 | "pretty": true, 18 | "outDir": "ts-output" 19 | }, 20 | "include": [ 21 | "src/decko.d.ts", 22 | "tests/index.ts" 23 | ] 24 | } 25 | --------------------------------------------------------------------------------