Lean JavaScript Utilities as Micro-libraries
-
This is not another pure functional utility library. These are just a set of common useful patterns that have evolved working with JS.
-
Unfortunately the existing popular libraries (Underscore, Lodash, Ramda, Trine, is, etc) don't result in readable code that is easy to reason about. Instead of putting the data in the first, second, third argument or context variable, this library favours lambda's (partially applied functions) that complement the native Array operations, not compete with them.
For a flavour of what I mean, see:
fs.readdirSync(__dirname)
.filter(includes('js'))
.filter(not(is.in(core)))
.map(prepend(__dirname+'/'))
.map(log('deleting'))
.map(fs.unlinkSync)
mutations
.map(key('addedNodes'))
.map(to.arr)
.reduce(flatten)
.filter(by('nodeName', includes('-')))
.map(ripple.draw)
-
Each function is in it's own repo. This library just has an automated link to all of them. This has a few benefits:
- You can
npm i utilise
and finally write imports like <org>/<repo>
in each file. - You don't have to make up esoteric names due to lack of
<org>/<repo>
in npm. - You can use
utilise.js
to import everything (in your application). - You can use
{ utility } = require('utilise/pure')
to import what you need + tree shake. - You don't have to load a 0.5MB utility library just to use one function.
- You can be a lot smarter with dead code elimination, even if you include the base file (but not use everything).
-
There is no spraying your code with _.
everywhere, since these functions are largely first-class additions to the grammar of the language that make your code a lot more fluent.
-
These are mostly stable/fixed, a few new ones may still need experimenting with to get the API right.
-
A smaller set of high power-to-weight ratio functions are preferred over many, many different functions that do similar things.
-
Each micro-library is only just a few lines.
-
Each micro-library has 100% coverage. See badges below.
-
All browsers (IE >= 9) + node are supported. Tests are run on real browsers using popper.
-
There is no polyfilling done here. Recommend using polyfill.io where needed. Some libraries will fail tests (like promise) which assume a native API like Promise
, unless you shim first.
API Reference
Please also refer to the respective test.js
for more cases and examples.
all
Select all elements based on a CSS selector, piercing shadow boundaries if the browser supports it.
all('li.class')
Narrow search space by also passing in a node
all('li.class', ul)
append
Append something to a string
['lorem', 'ipsum']
.map(append('-foo'))
args
Cherry-pick arguments to pass to function by index. This is useful when iterating a list, and invoking a function which may be confused if passed the index and array arguments.
['lorem.txt', 'ipsum.txt']
.map(args(0)(fs.createWriteStream))
This would fail without args
since the second argument (index) would try to be read as the encoding.
You can pick out more than one argument using an array instead of a number.
attr
Get or set value of element attribute.
attr('key')(el)
attr('key', 'value')(el)
attr('key', false)(el)
az
Sorts array ascendingly based on the value of a key
array.sort(az('value'))
Since it uses key internally, you can also sort on a deep key.
body
Get the value of a resource from a ripple instance
body(ripple)('users')
This is used internally to avoid any type-specific convienience defaults. Always returns undefined if the resource does not exist. You should probably use ripple('resource')
to get the value of a resource in your application.
by
Checks if a property matches a value
users = [ { name: 'foo' }, { name: 'bar' } ]
users
.filter(by('name', 'foo'))
If the second value parameter is a function, you can run custom logic against each property (default is ==
)
nodes
.filter(by('nodeName', isCustomElement))
It is common to sometimes see filtering empty values via .filter(Boolean)
. If only one parameter is given, it filters out objects that do not have a value for the property.
nodes
.filter(by('prop'))
chainable
Takes a function, and returns a new function that evaluates the original function and also returns it again ignoring what the evaluated function returns
ripple('key', value)
ripple.resource = chainable(ripple)
ripple
.resource('foo', 'bar')
.resource('lorem', 'ipsum')
NB: I think this will be deprecated in favour of the more generic proxy function that is used to alter return values
client
Simple variable: Am I on the server or browser?
client == true
client == false
Useful for isomorphic apps/libs, and also deadcode elimination.
clone
Returns a new deep copy of an object
copied = clone(original)
colorfill
Adds color to strings, standardising behaviour across server/client
require('colorfill')
'foo'.red
'foo'.red
copy
Copies properties from one object to another
keys(from)
.filter(not(is.in(private)))
.map(copy(from, to))
datum
Returns the D3 datum for a node. Useful in lists as you cannot d3.select(node).datum()
nodes
.map(datum)
deb
Lightweight scoped version of console.log
with a prefix, useful for per module identification
deb = deb('[module/prefix]')
deb('something went wrong!')
Returns the input, so it is useful with intermediary logging whilst iterating over a list
list
.map(op1)
.map(deb)
.map(op2)
You can filter debug logs on server-side with DEBUG
(module
, or module/submodule
) or using the debug
querystring parameter on the client-side.
debounce
Returns a debounced function. Specify time in ms, or defaults to 100ms.
debounced = debounce(fn)
debounced = debounce(200)(fn)
def
Defines a property, if does not already exist, returning the value
def(object, prop, value[, writable])
defaults
Sets default values for properties on an object if not already defined. Normally used at the beginning of components to default state values and expose API:
var state = defaults(this.state, {
values: []
, focused: true
})
To idempotently define API on an element:
defaults(this, { toggle, spin })
In case you need to default and reference a property for another property, you can default them individually rather than via an object literal.
defaults(state, 'numbers', [1,2,3,4,5])
defaults(state, 'odd', state.numbers.filter(d => d % 2))
delay
Creates a promise that eventually delays after the specified milliseconds. You can also set to resolve to a specific value.
await delay(1000)
await delay(1000, 'some value')
done
Given a versioned object, attaches a one-time callback for a response on the latest change. This is an alternative and more robust pattern than using random correlation ID's to link request-responses over a decoupled channel.
done(push(newUser)(users))
(d => !d.invalid && showConfirmation())
el
Creates a node from a CSS selector
el(div.foo.bar[lorem=ipsum])
emitterify
Enhance any object with .on
, .once
and .emit
var o = emitterify({})
o.on('event')
o.on('event', fn)
o.once('event', fn)
o.on('event.ns', fn)
o.emit('event', payload)
o.emit('event', [array])
err
Lightweight scoped version of console.error
with a prefix, useful for per module identification
err = err('[module/prefix]')
err('something went wrong!')
escape
Escapes HTML
escape = escape('<div></div>')
extend
Extends an object with properties from another, not overwriting properties by default. See also extend.
to = { foo: 1 }
from = { foo: 2, bar: 3 }
extend(to)(from)
falsy
Function that returns false
shouldIContinue = falsy
file
Reads and returns a file. Server only.
var template = file('template.html')
filify
Browserify transform that resolves file('filename')
to the actual file
file('foo')
filter
Filters an array
once(tr, filter(isEven))
first
Returns first element in array
first(array)
flatten
Flattens a 2D array
twoD = [[1], [2], [3]]
oneD = twoD.reduce(flatten)
form
Converts a <form>
to a JSON object as you would expect. This is useful to abstract the different ways of getting values from different controls (text inputs, radio elements, checkboxes, selects, files, custom elements). The keys come from the name
attributes. Checkboxes are represented as arrays. Files as FileList
. For Custom Elements, it takes the element.state.value
property, so they can also participate in forms.
const { values, invalid } = form(element)
values == {
foo: '..'
, bar: ['..', '..']
, baz: FileList
}
invalid
is an array containing all the elements that were marked by the is-invalid
class.
fn
Turns a function as a string into a real function
foo = 'function(){ console.log("Hi!") }'
foo = fn(foo)
foo()
from
Looks up and returns a property from an object. Useful for converting foreign keys to matching records.
users = [
{ name: 'foo', city: 1 }
, { name: 'bar', city: 2 }
, { name: 'baz', city: 1 }
]
cities: { 1: 'London', 2: 'New York', 3: 'Paris' }
users
.map(key('city'))
.map(from(cities))
.filter(unique)
from.parent
returns the value of a property from the parent datum. Useful if you generate a fixed number of columns, whose values depend on the parent.
processes = [
{ name: 'chrome', pid: '123', cpu: '50%' }
, { name: 'safari', pid: '456', cpu: '50%' }
]
once('tr', processes)
('td', ['name', 'pid', 'cpu'])
.text(from.parent)
<tr>
<td>chrome</td><td>123</td></td>50%</td>
</tr>
<tr>
<td>safari</td><td>456</td></td>50%</td>
</tr>
In general you should try to pass each element the data it needs and not reach outside of its own scope.
group
Grouped logging using groupCollapsed/groupEnd if it exists, or simple start/end demarcation logs using asterisk if not.
group('category', fn)
grep
Conditionally executes a function depending on the regex against its arguments. Returns the original unfiltered function. Useful for higher order modules to conditionally filter out logs of many smaller modules unobtrusively.
unfiltered = grep(console, 'log', /^(?!.*\[ri\/)/)
gt
Filters array depending if value of a key is greater than a threshold.
array.filter(gt(100, 'value'))
Since it uses key internally, you can also filter on a deep key.
has
Checks if object has property using in
keyword
has(object, 'prop')
hashcode
Converts string to unique numerical equivalent - same as Java hashcode function.
hashcode('foobar')
Extract the value of a header from a ripple resource
header('content-type')(resource)
Or if a second parameter is set, check for equality
resources
.filter(header('content-type', 'application/data'))
identity
Identity function, returns what it is passed in
identity(5)
iff
Only invoke second function if condition fulfilled. Useful for finer-grained control over skipping certain operations
sel(el).property('value', iff(cond)(setValue))
includes
Checks if string or array contains the pattern or element (uses indexOf common to strings and arrays)
files
.filter(includes('.js'))
is
Various basic flavours of checking
is(v)(d)
is.fn
is.str
is.num
is.obj
is.lit
is.bol
is.truthy
is.falsy
is.arr
is.null
is.def
is.promise
is.in(set)(d)
join
Replace a foreign key property with the full record or a value from the record
doctors
.map(join('shift', 'shifts'))
.map(join('speciality', 'specialities'))
.map(join('grade', 'grades.name'))
.map(join('hospital', 'hospitals.location'))
[1, 2, 3]
.map(join('grades'))
[1, 2, 3]
.map(join(grades))
If the second parameter is a string, it uses that as the ripple resource to look in. You can also use a primitive array outside of a ripple context.
key
Powerful versatile operator for accessing/setting key(s)
key('name')(d)
key(d => d.first + d.last)(d)
key('details.profile.name')(d)
key('details', 'foo')(d)
key('details.profile.name', 'foo')(d)
key(['name', 'city.name'])(d)
key()(d)
Accessing deep keys returns undefined
if a link is missing, which prevents doing things like:
(((d || {}).details || {}).profile || {}).name
Setting a deep key will create any missing keys it needs as it traverses the path.
If the second value parameter is a function, it evaluates it with the data before setting.
To make dates in all records human-readable with moment for example:
orders = [ { .. }, { .. } ]
orders
.map(key('date', mo.format('MMM Do YY')))
keys
Alias for Object.keys
keys({ foo: 1, bar: 2})
last
Returns the last element in the array
last(array)
link
Links changes in the attribute of one component to the attribute of another
link('events-calendar[selected-day]', 'event-detail[day]')
<events-calendar selected-day="1-1-1970" />
<event-detail day="1-1-1970"/>
lo
Lowercase a string
['A', 'B', 'C'].map(lo)
log
Lightweight scoped version of console.log
with a prefix, useful for per module identification
log = log('[module/prefix]')
log('something went wrong!')
Returns the input, so it is useful with intermediary logging whilst iterating over a list
list
.map(op1)
.map(log)
.map(op2)
lt
Filters array depending if value of a key is less than a threshold.
array.filter(lt(100, 'value'))
Since it uses key internally, you can also filter on a deep key.
mo
Convenience functions working with moment
dates.map(mo)
dates.map(mo.format('Do MMM YY'))
dates.map(mo.iso)
noop
Function that does nothing
;(fn || noop)()
not
Negates the result of a function
numbers
.filter(not(isEven))
.filter(not(is('5')))
nullify
Converts a truthy/falsy to true/null. This is a useful utility for D3 functions which expect a null value to remove as opposed to just a falsy.
selection
.attr('disabled', nullify(isDisabled))
once
Function for building entirely data-driven idempotent components/UI with D3.
once(node)
('div', { results: [1, 2, 3] })
('li', key('results'))
('a', inherit)
.text(String)
The first time you call once(node | string)
it essentially selects that element and limits the scope of subsequent operations to that.
Subsequents calls generate a D3 join using the syntax (selector, data)
. The selector can be:
- A selector string (
foo.bar.baz
). Classes are fine too and will be added to the final elements created. - A real element, which will be replicated.
- A function, which will be given parent data, in case you wish to output different (custom) elements based on data.
The data is the same as what you would normally use to generate a join (array of items, or function), with some convenient defaults: if you pass an object, number or boolean it'll be converted to a single-valued array, meaning "create one element with this as the datum". If you pass in a falsy, it defaults to empty array "meaning removing all elements of this type".
The return value is essentially a D3 join selection (enter/update/exit), so you can continue to customise using .text
, .classed
, .attr
, etc. You can also access the elements added via .enter
and removed via .exit
.
There are two further optional arguments you can use (selector, data[, key[, before]])
. The key function has the exact same meaning as normal (how to key data), which D3 defaults to by index. The before parameter can be used to force the insertion before a specific element à la .insert(something, before)
as opposed to just .append(something)
.
Once will also emitterify elements as well as the selection so you can fluently listen/proxy events. You can create custom events, use namespaces for unique listeners, and listeners on events like "click" will trigger from both real user interaction from the DOM as well as via .emit
.
once('list-component', 1)
.on('selected', d => alert(d))
once('list-component')
('.items', [1,2,3])
.on('click', d => this.parentNode.emit('selected', d))
overwrite
Extends an object with properties from another, overwriting existing properties. See also extend.
to = { foo: 1 }
from = { foo: 2, bar: 3 }
overwrite(to)(from)
owner
Either window or global dependeing on executing context
owner == window
owner == global
parse
Equivalent to JSON.parse
patch
Updates multiple values at a key, updates the internal log (if versioned), and emits a standardised change event (if emitterified). See also other functional versioned operators.
patch('key', { a, b, c })(object)
pause
Actually pauses a stream so you can build up a pipeline, pass it around, attach more pipes, before starting the flow. Server only.
var stream = pause(browserify)
.pipe(via(minify))
.pipe(via(deadcode))
addMorePipes(stream)
function addMorePipes(stream){
stream
.on('end', doSomething)
.pipe(file)
.flow()
}
perf
Completely unobtrusive way to evaluate how long a function takes in milliseconds.
If you have the following function call:
fn(args)
You wrap a perf
around fn
to see how long it takes:
perf(fn)(args)
You can also add an optional message to the log:
perf(fn, 'foo')(args)
pop
Pops an element from an array, updates the internal log (if versioned), and emits a standardised change event (if emitterified). See also other functional versioned operators.
pop(users)
prepend
Prepend something to a string
['foo', 'bar']
.map(prepend('hi-'))
promise
Convenience functions for working with (native) Promises
var p = promise()
p.resolve('result')
p.reject('something went wrong')
promise(5)
promise.args(1)('foo', 'bar')
promise.sync(1)('foo', 'bar')
promise.noop()
promise.null()
proxy
Proxy a function. It is common to use fn.apply(this, arguments)
for proxying. This function allows you to do that, but alter the return value and/or context.
proxy(fn, 5)
proxy(fn, 5, {})
This is also useful for functional inheritance:
bill.total = proxy(bill.subtotal, bill.vat)
push
Pushes an element to an array, updates the internal log (if versioned), and emits a standardised change event (if emitterified). See also other functional versioned operators.
push({ firstname: 'foo', lastname: 'bar' })(users)
ready
Calls the function once document.body
is ready or immediately if it already is
ready(fn)
raw
Select an element based on a CSS selector, piercing shadow boundaries if the browser supports it.
raw('.foo')
Narrow search space by also passing in a node
raw('li', ul)
rebind
D3 rebind function to rebind accessors. See the docs here.
remove
Removes a key from an object, updates the internal log (if versioned), and emits a standardised change event (if emitterified). See also other functional versioned operators.
remove('key')(object)
replace
Replace a value in a string
['Hi name', 'Bye name']
.map(replace('name', 'foo'))
resourcify
Returns the specified resource from a ripple instance. Returns an object of resources if multiple specified. Returns undefined
if one of the resources not present.
resourcify(ripple)('foo')
resourcify(ripple)('foo bar')
resourcify(ripple)('foo bar baz')
sall
Convenience function for d3.selectAll
. If either argument is already a D3 selection, it will not double wrap it.
sall(parent)(selector)
Parent/selector can be selection/string/node. If no parent, selects globally.
sel
Convenience function for d3.select
. If the argument is already a D3 selection, it will not double wrap it.
sel(string)
send
Sends a file on an express route. Server only.
app.get('/file', send('./file'))
set
Takes an atomic diff and applies it to an object, updating the internal log, and emitting a standardised change event (if emitterified). An atomic diff is an object in the format { type, key, value, time }
where type can be either of add | update | remove
.
set({ key, value, type })(object)
If there is no diff, it initialises object
with the .log
property:
set()(object[[, existing], max])
If an existing
object is specified with a .log
property, it will branch off that history to create the new .log
property. If a max
property is specified, there are three options:
-
0: Diffs will be pushed onto the .log
- = 0: The
.log
property will always be []
- < 0: Nulls will be pushed onto the
.log
(this is to avoid potentially expensive operations in the critical path, whilst still being able to use .log.length
as a revision counter)
Note that this, as will all utilities here, is fully graceful and will work with:
- A vanilla object - just applies diff
- An emitterified object i.e. has a .on/.emit - applies the diff and emits log event
- An emitterified, versioned object (has a .log) - applies diff, emits event, and updates immutable/diff log
See also the more ergonomic functional versioned operators which use this generic operator, but set most of the values for you:
slice
Slice all strings in an array
['Hi name', 'Bye name']
.map(slice(0, 2))
sort
Sorts elements in an array
once('tr', sort(Boolean))
split
Split items
['a.b', 'b.c'].map(split('.'))
str
Coerces anything into a string
str(5)
str({ foo: 5 })
str(undefined)
str(function(){ .. })
stripws
Strips the whitespace between tags (but not between attributes), which is useful to make declarative testing less brittle and less sensitive
stripws`
<div>
foo
</div>
`
stripws('<a>\n <b>')
tdraw
Simple stub for the draw
function to test components. Takes a DOM element, a render function and some initial state. Sets the initial .state
, the .draw
function and renders the element.
t.plan(2)
const host = tdraw(el('ux-button'), button, { label: 'foo', spinning: true })
host.spin(true)
t.ok(includes(`class="is-spinning"`)(host.outerHTML))
host.spin(false)
t.notOk(includes(`class="is-spinning"`)(host.outerHTML))
th
Invokes a function with the this
variable, then the arguments. Useful for using arrow syntax with functions that need the context variable.
const log = el => d => console.log(el.value)
input.on('keyup', th(log))
time
Alias for setTimeout
with duration as first parameter for better readability
time(10, function(){ .. })
time(20, function(){ .. })
time(30, function(){ .. })
to
to.arr: Converts to a primitive type (only real arrays)
to.arr(NodeList)
to.arr(arguments)
to.obj: Converts an array to an object. Uses id
property as key by default if none specified. The key can also be a function which will receive the item and it's index as parameters, and return the computed string to use as the key.
[
{ id: 'foo', value: 1 }
, { id: 'bar', value: 2 }
].reduce(to.obj, {})
Note: You should always use an initial value with the reduce function. This is because if your array happens to be an array with only one element and there is no initial value, JavaScript will not even call the reduce function.
unique
Filter an array to unique values
[1,1,2,3].filter(unique, 1)
update
Updates a value at a key, updates the internal log (if versioned), and emits a standardised change event (if emitterified). See also other functional versioned operators.
update('key', value)(object)
values
Converts an object to array
values({
a: { name: 'foo', value: 1 }
, b: { name: 'bar', value: 2 }
})
via
Buffers output to a stream destination. Useful when you need the whole input rather than chunks. Server only.
stream
.pipe(via(minify))
.pipe(via(replace))
wait
Only invoke handler if condition fulfilled. Useful for determining execution based on declarative pattern matching.
o.once(wait(msg => msg.id)(handler))
wrap
Wraps something in a function which returns it when executed
wrapped = wrap(5)
wrapped()