tapsig
v0.1.7
Published
Tacks custom extensions onto existing JavaScript libraries
Downloads
10
Readme
Tapsig
This tiny library (0.8kb minified & gzipped) tacks custom extensions onto existing JavaScript functions and objects. That makes it incredibly easy to supplement existing JavaScript libraries with custom methods without touching its original code.
It works by wrapping the target in a Proxy. The Proxy sticks to the tapped library by attaching itself to properties accessed or methods called on it.
This package works in Node.js and in the browser. Note however that the browser must support ES2015 Proxies (which are not polyfillable), which leaves out IE11 in particular.
Table of Contents
Installation
Install it from npm:
npm install --save tapsig
Include in the Browser
You can use this package in your browser with one of the following snippets:
The most common version. Introduces a global
tapsig
variable, runs in all modern browsers:<script src="node_modules/tapsig/dist/browser.min.js"></script> <!-- or from CDN: --> <script src="https://unpkg.com/tapsig"></script>
If you're really living on the bleeding edge and use ES modules directly in the browser, you can
import
the package as well:import * as tapsig from "./node_modules/tapsig/dist/browser.esm.min.js" // or from CDN: import * as tapsig from "https://unpkg.com/tapsig/dist/browser.esm.min.js"
As opposed to the first snippet, this will not create a global
tapsig
function.
Include in Node.js
Include this package in Node.js like you usually do:
const tapsig = require('tapsig')
If you use --experimental-modules
, there's a .mjs
version, too:
import * as tapsig from 'tapsig/dist/node.esm'
Usage
Basic Example
Now that we have grabbed the tapsig
object, we can start injecting custom properties and methods into a library. Since most of us probably know jQuery, let's take that as an example.
Remember older jQuery versions? They had a size()
method that was removed in favor of the length
property.
Now let's re-implement that method. We do so by passing the library we want to wrap and a thing we call an "injection object" to the tap()
method:
const $ = tapsig.tap(jQuery, {
size () {
return this.length
}
})
$('div').size() // Returns some number
There are some things to note here:
Notice how the
size()
method is available not only on$
but also on$('div')
? That's the whole point of Tapsig: it reproduces and attaches itself recursively to every property or method you access on the originally tapped library.This also means that if we returned an object or a function from our
size()
method, that return value would also be tapped.The
this
context of thesize()
method (and any other method defined on the injection object) points to the tapped object the method it is called on — in our case that's the tapped$('div')
collection. If you want to access the underlying untapped object, use theuntap()
method.
Dynamic Injections
The first point noted at the end of the last section is a feature, but in our example it can be quite unhandy: In most cases, we want to inject our custom properties only under certain circumstances.
In the example above, the size()
method is not only available on the $('div')
, but also on the $
itself. However, $
is not a jQuery collection and thus $.size()
would return undefined
.
That's why we only want to provide the size()
method on a jQuery collection. For that purpose, we can inject a function instead of an object. The function decides on a case-by-case basis which properties to provide:
const $ = tapsig.tap(jQuery, target => {
// Only add the `size()` method on a jQuery collection
if (target instanceof jQuery) {
return {
size () {
return this.length
}
}
}
// If we don't return anything, no custom properties are added
})
$('div').size() // Still returns some number
$.size() // TypeError: $.size is not a function
Naming Conflicts
Injected custom properties will shadow existing ones. In other words, custom properties will always take precedence over builtin properties.
Inject Getters
You may provide getters in an injection object:
const $ = tapsig.tap(jQuery, {
get version () {
return jQuery.fn.jquery
}
})
Untapping
You can unwrap a tapped object with the untap()
method, e.g. if you need to use the original API in a method call:
const $ = tapsig.tap(jQuery, {
foo () {
// We want to check if there's a `foo` property
// in the tapped object:
return 'foo' in tapsig.untap(this)
}
})
Note: Both the
tap()
and theuntap()
methods are idempotent. Tapping an already tapped object won't do anything, just like untapping a non-tapped object will have no effect.
Catch-Missing and Catch-All
The Tapsig library exposes the ALL
and MISSING
symbols. You can use them as method names in the injection object to achieve certain behaviour.
MISSING
A method named with the MISSING
symbol will be used to handle property access for properties that are not explicitely defined in the injection object and not found in the original tapped API:
const $ = tapsig.tap(jQuery, {
foo: 'bar',
[tapsig.MISSING] (name) {
return `no such property '${name}'`
}
})
$.foo // "bar", as defined in the injection object
$.baz // "no such property 'baz'", returned by the MISSING method
$.ajax // The AJAX function from the jQuery library
ALL
A method named with the ALL
symbol will be used to handle every property access for properties that are not explicitely defined in the injection object. This means that the ALL
method takes precedence even over built-in properties.
const $ = tapsig.tap(jQuery, {
foo: 'bar',
[tapsig.ALL] (name) {
return `no such property '${name}'`
}
})
$.foo // "bar", as defined in the injection object
$.baz // "no such property 'baz'", returned by the ALL method
$.ajax // "no such property 'ajax'", returned by the ALL method
Warning: Be very careful when using the
ALL
method. It will be called to answer requests for JavaScript-Builtins likeprototype
, literally any property. This can lead to unexpected results, so you should always be aware and possibly quite restrictive about which properties you answer:
const $ = tapsig.tap(jQuery, {
[tapsig.ALL] (name) {
if (name.startsWith('foo_')) {
return // something you want to achieve with all `foo_` properties
} else {
return tapsig.untap(this)[name]
}
}
})
Note: The results of both the
MISSING
andALL
method will be tapped before they go back to the user.
Checking if an Object is Tapped
You can check if an object is tapped by running tapsig.isTapped(object)
.
Masking Values
By default, all injected properties and all results returned from injected functions will be tapped.
If you want to prevent such a value to be tapped, you can use the mask()
method:
const $ = tapsig.tap(jQuery, {
originalJQuery () {
// Untap the proxy, then mask it
return tapsig.mask(tapsig.untap(this))
}
})
// $.originalJQuery() === jQuery
Tap Promises
There are edge cases where wrapping an object in a Proxy does not work. Promises are one of those.
Therefore, tapsig
will not tap Promises themselves, but wrap them in another Promise whose resolved result will in turn be tapped.
Debugging
The Node.js version of Tapsig uses the debug utility to print logs.
For the sake of bundle size, the browser build uses just a simple console.log()
. As opposed to the Node.js logs it has to be enabled manually by setting the verbose
parameter (3rd parameter of tapsig.tap()
) to true
.
"Tapsig"?
It's hard these days to find a good module name that's available on npm. I wanted some playful name related to "tap" and came up with a word in my mother tongue: "tapsig" (roughly pronounced like "tub-zig") is German for "clumsy", the kind you can observe when kittens or toddlers practice their first steps. 🐾