summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.babelrc7
-rw-r--r--.eslintrc50
-rw-r--r--.gitignore4
-rw-r--r--.travis.yml1
-rw-r--r--LICENSE20
-rw-r--r--README.md120
-rw-r--r--package.json33
-rw-r--r--src/decko.d.ts30
-rw-r--r--src/decko.js98
-rw-r--r--tests/bind.js37
-rw-r--r--tests/debounce.js71
-rw-r--r--tests/index.ts17
-rw-r--r--tests/memoize.js72
-rw-r--r--tsconfig.json24
14 files changed, 584 insertions, 0 deletions
diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..55dee01
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,7 @@
+{
+ "modules": "umd",
+ "loose": "all",
+ "compact": true,
+ "comments": false,
+ "stage": 0
+}
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..bb86529
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,50 @@
+{
+ "parser": "babel-eslint",
+ "extends": "eslint:recommended",
+ "env": {
+ "browser": true
+ },
+ "ecmaFeatures": {
+ "modules": true,
+ "jsx": true
+ },
+ "rules": {
+ "no-unused-vars": [1, { "args": "after-used" }],
+ "no-cond-assign": 1,
+ "semi": 2,
+ "camelcase": 0,
+ "comma-style": 2,
+ "comma-dangle": [2, "never"],
+ "indent": [2, "tab", {"SwitchCase": 1}],
+ "no-mixed-spaces-and-tabs": [2, "smart-tabs"],
+ "no-trailing-spaces": [2, { "skipBlankLines": true }],
+ "max-nested-callbacks": [2, 3],
+ "no-eval": 2,
+ "no-implied-eval": 2,
+ "no-new-func": 2,
+ "guard-for-in": 2,
+ "eqeqeq": 2,
+ "no-else-return": 2,
+ "no-redeclare": 2,
+ "no-dupe-keys": 2,
+ "radix": 2,
+ "strict": [2, "never"],
+ "no-shadow": 0,
+ "callback-return": [1, ["callback", "cb", "next", "done"]],
+ "no-delete-var": 2,
+ "no-undef-init": 2,
+ "no-shadow-restricted-names": 2,
+ "handle-callback-err": 0,
+ "no-lonely-if": 2,
+ "space-return-throw-case": 2,
+ "constructor-super": 2,
+ "no-this-before-super": 2,
+ "no-dupe-class-members": 2,
+ "no-const-assign": 2,
+ "prefer-spread": 2,
+ "no-useless-concat": 2,
+ "no-var": 2,
+ "object-shorthand": 2,
+ "prefer-arrow-callback": 2
+ }
+}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1e1036a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+npm-debug.log
+node_modules
+dist
+.DS_Store
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..587bd3e
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1 @@
+language: node_js
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a38549a
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 Jason Miller
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..20f132b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,120 @@
+# 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)
+
+A concise implementation of the three most useful [decorators](https://github.com/wycats/javascript-decorators):
+
+- `@bind`: make the value of `this` constant within a method
+- `@debounce`: throttle calls to a method
+- `@memoize`: cache return values based on arguments
+
+Decorators help simplify code by replacing the noise of common patterns with declarative annotations.
+Conversely, decorators can also be overused and create obscurity.
+Decko establishes 3 standard decorators that are immediately recognizable, so you can avoid creating decorators in your own codebase.
+
+> 💡 **Tip:** decko is particularly well-suited to [**Preact Classful Components**](https://github.com/developit/preact).
+>
+> 💫 **Note:**
+> - For Babel 6+, be sure to install [babel-plugin-transform-decorators-legacy](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy).
+> - For Typescript, be sure to enable `{"experimentalDecorators": true}` in your tsconfig.json.
+
+
+## Installation
+
+Available on [npm](https://npmjs.com/package/decko):
+
+```sh
+npm i -S decko
+```
+
+
+## Usage
+
+Each decorator method is available as a named import.
+
+```js
+import { bind, memoize, debounce } from 'decko';
+```
+
+
+### `@bind`
+
+```js
+class Example {
+ @bind
+ foo() {
+ // the value of `this` is always the object from which foo() was referenced.
+ return this;
+ }
+}
+
+let e = new Example();
+assert.equal(e.foo.call(null), e);
+```
+
+
+
+### `@memoize`
+
+> Cache values returned from the decorated function.
+> Uses the first argument as a cache key.
+> _Cache keys are always converted to strings._
+>
+> ##### Options:
+>
+> `caseSensitive: false` - _Makes cache keys case-insensitive_
+>
+> `cache: {}` - _Presupply cache storage, for seeding or sharing entries_
+
+```js
+class Example {
+ @memoize
+ expensive(key) {
+ let start = Date.now();
+ while (Date.now()-start < 500) key++;
+ return key;
+ }
+}
+
+let e = new Example();
+
+// this takes 500ms
+let one = e.expensive(1);
+
+// this takes 0ms
+let two = e.expensive(1);
+
+// this takes 500ms
+let three = e.expensive(2);
+```
+
+
+
+### `@debounce`
+
+> Throttle calls to the decorated function. To debounce means "call this at most once per N ms".
+> 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.
+>
+> ##### Options:
+>
+> `delay: 0` - _The number of milliseconds to buffer calls for._
+
+```js
+class Example {
+ @debounce
+ foo() {
+ return this;
+ }
+}
+
+let e = new Example();
+
+// this will only call foo() once:
+for (let i=1000; i--) e.foo();
+```
+
+
+---
+
+License
+-------
+
+MIT
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..c5cf1dc
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "decko",
+ "version": "1.2.0",
+ "main": "dist/decko.js",
+ "types": "dist/decko.d.ts",
+ "description": "A collection of the most useful property decorators.",
+ "scripts": {
+ "build": "mkdir -p dist && babel -f src/decko.js -s -o $npm_package_main < src/decko.js && npm run build:ts",
+ "build:ts": "cp src/decko.d.ts dist/",
+ "test": "npm run test:ts && eslint {src,tests}/**.js && mocha --compilers js:babel/register tests/**/*.js",
+ "test:ts": "tsc -p ./",
+ "style:ts": "tsfmt -r",
+ "prepublish": "npm run build",
+ "release": "npm run build && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish"
+ },
+ "files": [
+ "src",
+ "dist"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/developit/decko.git"
+ },
+ "devDependencies": {
+ "babel": "^5.8.21",
+ "babel-eslint": "^4.1.6",
+ "chai": "^3.2.0",
+ "eslint": "^1.10.3",
+ "mocha": "^2.3.0",
+ "typescript": "2.1.6",
+ "typescript-formatter": "4.1.1"
+ }
+}
diff --git a/src/decko.d.ts b/src/decko.d.ts
new file mode 100644
index 0000000..e65c737
--- /dev/null
+++ b/src/decko.d.ts
@@ -0,0 +1,30 @@
+/**
+ *
+ */
+export function bind<T>(
+ target: Object,
+ propertyKey: string | symbol,
+ descriptor?: TypedPropertyDescriptor<T>
+): TypedPropertyDescriptor<T> | void;
+export function bind(): MethodDecorator;
+
+/**
+ * @param caseSensitive Makes cache keys case-insensitive
+ * @param cache Presupply cache storage, for seeding or sharing entries
+ */
+
+export function memoize<T>(
+ target: Object,
+ propertyKey: string | symbol,
+ descriptor?: TypedPropertyDescriptor<T>
+): TypedPropertyDescriptor<T> | void;
+export function memoize(caseSensitive?: boolean, cache?: Object): MethodDecorator;
+/**
+ * @param delay number
+ */
+export function debounce<T>(
+ target: Object,
+ propertyKey: string | symbol,
+ descriptor?: TypedPropertyDescriptor<T>
+): TypedPropertyDescriptor<T> | void;
+export function debounce(delay?: number): MethodDecorator; \ No newline at end of file
diff --git a/src/decko.js b/src/decko.js
new file mode 100644
index 0000000..298e092
--- /dev/null
+++ b/src/decko.js
@@ -0,0 +1,98 @@
+
+const EMPTY = {};
+const HOP = Object.prototype.hasOwnProperty;
+
+let fns = {
+ /** let cachedFn = memoize(originalFn); */
+ memoize(fn, opt=EMPTY) {
+ let cache = opt.cache || {};
+ return function(...a) {
+ let k = String(a[0]);
+ if (opt.caseSensitive===false) k = k.toLowerCase();
+ return HOP.call(cache,k) ? cache[k] : (cache[k] = fn.apply(this, a));
+ };
+ },
+
+ /** let throttled = debounce(10, console.log); */
+ debounce(fn, opts) {
+ if (typeof opts==='function') { let p = fn; fn = opts; opts = p; }
+ let delay = opts && opts.delay || opts || 0,
+ args, context, timer;
+ return function(...a) {
+ args = a;
+ context = this;
+ if (!timer) timer = setTimeout( () => {
+ fn.apply(context, args);
+ args = context = timer = null;
+ }, delay);
+ };
+ },
+
+ bind(target, key, { value: fn }) {
+ return {
+ configurable: true,
+ get() {
+ let value = fn.bind(this);
+ Object.defineProperty(this, key, {
+ value,
+ configurable: true,
+ writable: true
+ });
+ return value;
+ }
+ };
+ }
+};
+
+
+let memoize = multiMethod(fns.memoize),
+ debounce = multiMethod(fns.debounce),
+ bind = multiMethod((f,c)=>f.bind(c), ()=>fns.bind);
+
+export { memoize, debounce, bind };
+export default { memoize, debounce, bind };
+
+
+/** Creates a function that supports the following calling styles:
+ * d() - returns an unconfigured decorator
+ * d(opts) - returns a configured decorator
+ * d(fn, opts) - returns a decorated proxy to `fn`
+ * d(target, key, desc) - the decorator itself
+ *
+ * @Example:
+ * // simple identity deco:
+ * let d = multiMethod( fn => fn );
+ *
+ * class Foo {
+ * @d
+ * bar() { }
+ *
+ * @d()
+ * baz() { }
+ *
+ * @d({ opts })
+ * bat() { }
+ *
+ * bap = d(() => {})
+ * }
+ */
+function multiMethod(inner, deco) {
+ deco = deco || inner.decorate || decorator(inner);
+ let d = deco();
+ return (...args) => {
+ let l = args.length;
+ return (l<2 ? deco : (l>2 ? d : inner))(...args);
+ };
+}
+
+/** Returns function supports the forms:
+ * deco(target, key, desc) -> decorate a method
+ * deco(Fn) -> call the decorator proxy on a function
+ */
+function decorator(fn) {
+ return opt => (
+ typeof opt==='function' ? fn(opt) : (target, key, desc) => {
+ desc.value = fn(desc.value, opt, target, key, desc);
+ }
+ );
+}
diff --git a/tests/bind.js b/tests/bind.js
new file mode 100644
index 0000000..c76a0f4
--- /dev/null
+++ b/tests/bind.js
@@ -0,0 +1,37 @@
+import { bind } from '..';
+import { expect } from 'chai';
+
+/*global describe,it*/
+
+describe('bind()', () => {
+ it('should bind when used as a simple decorator', next => {
+ let c = {
+ @bind
+ foo() {
+ return this;
+ }
+ };
+
+ expect(c.foo()).to.equal(c);
+
+ let p = c.foo;
+ expect(p()).to.equal(c);
+
+ let a = {};
+ expect(c.foo.call(a)).to.equal(c);
+
+ next();
+ });
+
+ it('should bind when used as a function', next => {
+ let ctx = {},
+ c = bind(function(){ return this; }, ctx);
+
+ expect(c()).to.equal(ctx);
+
+ let a = {};
+ expect(c.call(a)).to.equal(ctx);
+
+ next();
+ });
+});
diff --git a/tests/debounce.js b/tests/debounce.js
new file mode 100644
index 0000000..1c8bcf0
--- /dev/null
+++ b/tests/debounce.js
@@ -0,0 +1,71 @@
+import { debounce } from '..';
+import { expect } from 'chai';
+
+/*global describe,it*/
+
+describe('debounce()', () => {
+ it('should debounce when used as a simple decorator', next => {
+ let c = {
+ calls: 0,
+ args: null,
+
+ @debounce
+ foo(...args) {
+ c.calls++;
+ c.args = args;
+ c.context = this;
+ }
+ };
+
+ expect(c).to.have.property('calls', 0);
+ c.foo(1);
+ expect(c).to.have.property('calls', 0);
+ c.foo(2);
+ c.foo(3);
+ setTimeout( () => {
+ expect(c).to.have.property('calls', 1);
+ expect(c.args).to.deep.equal([3]);
+ expect(c.context).to.equal(c);
+
+ next();
+ }, 20);
+ });
+
+ it('should debounce when used as a function', next => {
+ let c = debounce( (...args) => {
+ m.calls++;
+ m.args = args;
+ }),
+ m = { calls:0, args:null };
+
+ expect(m).to.have.property('calls', 0);
+ c(1);
+ expect(m).to.have.property('calls', 0);
+ c(2);
+ c(3);
+ setTimeout( () => {
+ expect(m).to.have.property('calls', 1);
+ expect(m.args).to.deep.equal([3]);
+
+ next();
+ }, 20);
+ });
+
+ it('should support passing a delay', next => {
+ let c = debounce(5, (...args) => {
+ m.calls.push(args);
+ }),
+ m = { calls:[] };
+
+ c(1);
+ setTimeout(()=> c(2), 1);
+ setTimeout(()=> c(3), 10);
+ setTimeout(()=> c(4), 14);
+ setTimeout(()=> c(5), 22);
+ expect(m.calls).to.have.length(0);
+ setTimeout( () => {
+ expect(m.calls).to.deep.equal([ [2], [4], [5] ]);
+ next();
+ }, 30);
+ });
+});
diff --git a/tests/index.ts b/tests/index.ts
new file mode 100644
index 0000000..14c4efe
--- /dev/null
+++ b/tests/index.ts
@@ -0,0 +1,17 @@
+import { bind, debounce, memoize } from '..';
+class C {
+ @bind
+ foo() { }
+
+ @debounce
+ moo() { }
+
+ @debounce(1000)
+ mooWithCustomDelay() { }
+
+ @memoize
+ mem() { }
+
+ @memoize(true)
+ memWithConfig() { }
+} \ No newline at end of file
diff --git a/tests/memoize.js b/tests/memoize.js
new file mode 100644
index 0000000..98f3678
--- /dev/null
+++ b/tests/memoize.js
@@ -0,0 +1,72 @@
+import { memoize } from '..';
+import { expect } from 'chai';
+
+/*global describe,it*/
+
+describe('memoize()', () => {
+ it('should memoize when used as a simple decorator', next => {
+ let c = {
+ @memoize
+ foo(key) {
+ c[key] = (c[key] || 0) + 1;
+ }
+ };
+
+ expect(c).not.to.have.property('a');
+ c.foo('a');
+ expect(c).to.have.property('a', 1);
+ c.foo('a');
+ c.foo('a');
+ expect(c).to.have.property('a', 1);
+
+ next();
+ });
+
+ it('should memoize when used as a function', next => {
+ let c = memoize( key => {
+ m[key] = (m[key] || 0) + 1;
+ }),
+ m = {};
+
+ expect(m).not.to.have.property('a');
+ c('a');
+ expect(m).to.have.property('a', 1);
+ c('a');
+ c('a');
+ expect(m).to.have.property('a', 1);
+
+ next();
+ });
+
+ it('should memoize when called without arguments', next => {
+ let c = memoize( key => {
+ m[key] = (m[key] || 0) + 1;
+ }),
+ m = {};
+
+ expect(m).not.to.have.property('undefined');
+ c();
+ expect(m).to.have.property('undefined', 1);
+ c();
+ c();
+ expect(m).to.have.property('undefined', 1);
+
+ next();
+ });
+
+ it('should memoize when called with an empty string', next => {
+ let c = memoize( key => {
+ m[key] = (m[key] || 0) + 1;
+ }),
+ m = {};
+
+ expect(m).not.to.have.property('');
+ c('');
+ expect(m).to.have.property('', 1);
+ c('');
+ c('');
+ expect(m).to.have.property('', 1);
+
+ next();
+ });
+});
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..f78fc76
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "module": "es2015",
+ "target": "es2016",
+ "lib": [
+ "dom",
+ "es2016"
+ ],
+ "baseUrl": "./",
+ "noImplicitAny": true,
+ "experimentalDecorators": true,
+ "sourceMap": false,
+ "moduleResolution": "node",
+ "strictNullChecks": true,
+ "declaration": true,
+ "noEmit": true,
+ "pretty": true,
+ "outDir": "ts-output"
+ },
+ "include": [
+ "src/decko.d.ts",
+ "tests/index.ts"
+ ]
+}