├── .npmrc ├── cjs ├── package.json └── index.js ├── test ├── package.json ├── index.js └── index.html ├── .gitignore ├── .npmignore ├── .travis.yml ├── min.js ├── rollup └── babel.config.js ├── esm └── index.js ├── index.js ├── LICENSE ├── package.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .travis.yml 4 | node_modules/ 5 | rollup/ 6 | test/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | git: 5 | depth: 1 6 | branches: 7 | only: 8 | - master 9 | - /^greenkeeper/.*$/ 10 | after_success: 11 | - "npm run coveralls" 12 | -------------------------------------------------------------------------------- /min.js: -------------------------------------------------------------------------------- 1 | self.boundOnce=function(n){"use strict";var t=new WeakMap;return function(n,e){var r="function"==typeof e?e:n[e],u=t.get(n)||function(n,e){return t.set(n,e),e}(n,new Map);return u.get(r)||function(n,t,e){return n.set(t,e),e}(u,r,r.bind(n))}}(); -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const bound = require('../cjs'); 2 | 3 | const object = {}; 4 | const fn = bound(object, method); 5 | 6 | console.assert(fn() === object, 'unexpected return'); 7 | 8 | const own = {method}; 9 | console.assert(bound(own, 'method') === bound(own, 'method'), 'unexpected duplication'); 10 | 11 | function method() { 12 | return this; 13 | } 14 | -------------------------------------------------------------------------------- /rollup/babel.config.js: -------------------------------------------------------------------------------- 1 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 2 | import babel from '@rollup/plugin-babel'; 3 | 4 | export default { 5 | input: './esm/index.js', 6 | plugins: [ 7 | 8 | nodeResolve(), 9 | babel({ 10 | presets: ['@babel/preset-env'], 11 | babelHelpers: 'bundled' 12 | }) 13 | ], 14 | 15 | output: { 16 | exports: 'named', 17 | file: './index.js', 18 | format: 'iife', 19 | name: 'boundOnce' 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | const _ = new WeakMap; 2 | const add = (context, methods) => { 3 | _.set(context, methods); 4 | return methods; 5 | }; 6 | const set = (methods, method, bound) => { 7 | methods.set(method, bound); 8 | return bound; 9 | }; 10 | export default (context, fn) => { 11 | const method = typeof fn === 'function' ? fn : context[fn]; 12 | const methods = _.get(context) || add(context, new Map); 13 | return methods.get(method) || set(methods, method, method.bind(context)); 14 | }; 15 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const _ = new WeakMap; 3 | const add = (context, methods) => { 4 | _.set(context, methods); 5 | return methods; 6 | }; 7 | const set = (methods, method, bound) => { 8 | methods.set(method, bound); 9 | return bound; 10 | }; 11 | module.exports = (context, fn) => { 12 | const method = typeof fn === 'function' ? fn : context[fn]; 13 | const methods = _.get(context) || add(context, new Map); 14 | return methods.get(method) || set(methods, method, method.bind(context)); 15 | }; 16 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | bound-once 7 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | self.boundOnce = (function (exports) { 2 | 'use strict'; 3 | 4 | var _ = new WeakMap(); 5 | 6 | var add = function add(context, methods) { 7 | _.set(context, methods); 8 | 9 | return methods; 10 | }; 11 | 12 | var set = function set(methods, method, bound) { 13 | methods.set(method, bound); 14 | return bound; 15 | }; 16 | 17 | var index = (function (context, fn) { 18 | var method = typeof fn === 'function' ? fn : context[fn]; 19 | var methods = _.get(context) || add(context, new Map()); 20 | return methods.get(method) || set(methods, method, method.bind(context)); 21 | }); 22 | 23 | 24 | 25 | return index; 26 | 27 | }({})); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2020, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bound-once", 3 | "version": "0.1.3", 4 | "description": "A fast, memory efficient, and tiny solution to an evergreen problem", 5 | "main": "./cjs/index.js", 6 | "scripts": { 7 | "build": "npm run cjs && npm run rollup:babel && npm run fix:default && npm run min && npm run test", 8 | "cjs": "ascjs --no-default esm cjs", 9 | "rollup:babel": "rollup --config rollup/babel.config.js && sed -i.bck 's/^var /self./' index.js && rm -rf index.js.bck", 10 | "min": "terser index.js --comments='/^!/' -c -m -o min.js", 11 | "fix:default": "sed -i 's/exports.default = index;//' index.js && sed -i 's/return exports/return index/' index.js && sed -i 's/({})/({}).default/' min.js", 12 | "coveralls": "nyc report --reporter=text-lcov | coveralls", 13 | "test": "nyc node test/index.js" 14 | }, 15 | "keywords": [ 16 | "bind", 17 | "bound", 18 | "once" 19 | ], 20 | "author": "Andrea Giammarchi", 21 | "license": "ISC", 22 | "devDependencies": { 23 | "@babel/core": "^7.11.6", 24 | "@babel/preset-env": "^7.11.5", 25 | "@rollup/plugin-babel": "^5.2.1", 26 | "@rollup/plugin-node-resolve": "^9.0.0", 27 | "ascjs": "^4.0.1", 28 | "coveralls": "^3.1.0", 29 | "nyc": "^15.1.0", 30 | "rollup": "^2.27.1", 31 | "rollup-plugin-terser": "^7.0.2", 32 | "terser": "^5.3.1" 33 | }, 34 | "module": "./esm/index.js", 35 | "type": "module", 36 | "exports": { 37 | ".": { 38 | "import": "./esm/index.js", 39 | "default": "./cjs/index.js" 40 | }, 41 | "./package.json": "./package.json" 42 | }, 43 | "unpkg": "min.js", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/WebReflection/bound-once.git" 47 | }, 48 | "bugs": { 49 | "url": "https://github.com/WebReflection/bound-once/issues" 50 | }, 51 | "homepage": "https://github.com/WebReflection/bound-once#readme" 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bound-once 2 | 3 | [![Build Status](https://travis-ci.com/WebReflection/bound-once.svg?branch=master)](https://travis-ci.com/WebReflection/bound-once) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/bound-once/badge.svg?branch=master)](https://coveralls.io/github/WebReflection/bound-once?branch=master) 4 | 5 | **Social Media Photo by [Mattias Olsson](https://unsplash.com/@mattiaswolsson) on [Unsplash](https://unsplash.com/)** 6 | 7 | A fast, memory efficient, and tiny solution to an evergreen problem. 8 | 9 | Alternatively, you can check [bind.for](https://github.com/WebReflection/bind.for#readme) utility, which is definitively faster, but it has not ideal ergonomics. 10 | 11 | ## API 12 | 13 | ```js 14 | const once = bound( 15 | context, // the context to bind 16 | method // a string, resolved as context[method] 17 | // or a function/method/callback to bind 18 | ); 19 | 20 | once(); // will have the right context 21 | ``` 22 | 23 | 24 | ### Usage Example 25 | 26 | ```js 27 | import bound from 'bound-once'; 28 | // const bound = require('bound-once'); 29 | // 30 | 31 | class Counter { 32 | constructor() { 33 | this.count = 0; 34 | } 35 | increment() { 36 | this.count++; 37 | console.log(this.count); 38 | } 39 | } 40 | 41 | const counter = new Counter; 42 | 43 | // bound(counter, 'increment') always returns the same bound method 44 | // so it is safe to use it for listeners, as it'll be added only once 45 | document.body.addEventListener('click', bound(counter, 'increment')); 46 | document.body.addEventListener('click', bound(counter, 'increment')); 47 | document.body.addEventListener('click', bound(counter, 'increment')); 48 | 49 | // example by passing a callback instead of a string 50 | const {increment} = Counter.prototype; 51 | bound(counter, 'increment') === bound(counter, increment); 52 | ``` 53 | 54 | ### Partial Application 55 | 56 | ```js 57 | class Component extends HTMLElement { 58 | // instead of assigning N methods bound 59 | // just assign once and happily bind it after 60 | #bound = bound.bind(null, this); 61 | connectedCallback() { 62 | this.addEventListener('click', this.#bound('onClick')); 63 | this.addEventListener('change', this.#bound('onChange')); 64 | this.addEventListener('input', this.#bound(this.onInput)); 65 | } 66 | disconnectedCallback() { 67 | this.removeEventListener('click', this.#bound('onClick')); 68 | this.removeEventListener('change', this.#bound('onChange')); 69 | this.removeEventListener('input', this.#bound(this.onInput)); 70 | } 71 | // any method 72 | onClick() {} 73 | onChange() {} 74 | onInput() {} 75 | } 76 | ``` 77 | 78 | This [Custom Element with closed ShadowDOM](https://codepen.io/WebReflection/pen/qBZMRxy?editors=0010) is also another practical application, avoiding constant listeners changes per each render call. 79 | --------------------------------------------------------------------------------