Fluent interface to provide an expressive syntax for common manipulations. Rather than enforcing a different paradigm, flooent builds upon and extends the native capabilities of various JavaScript objects.
Given you have logical, procedural, "hard to visualize" code like this:
// given we have const path = 'App/Controllers/user.js'
const filename = path.substring(path.lastIndexOf('/') + 1)
let name = filename.substring(0, filename.lastIndexOf('.'))
if (!name.endsWith('Controller')) name+= 'Controller'
return name.substring(0, 1).toUpperCase() + name.substring(1)
refactor it into plain English
// given we have const path = 'App/Controllers/user.js'
given.string(path)
.afterLast('/')
.beforeLast('.')
.endWith('Controller')
.capitalize()
.valueOf()
npm install flooent
Use given
to create either a flooent Number, Array, Map or String.
import { given } from 'flooent'
given.string('hello') // instance of Stringable
given.array([1, 2]) // instance of Arrayable
given.number(1) // instance of Numberable
given.map({ key: 'value' }) // or given.map([['key', 'value']]), or given.map(new Map(...)) | instance of Mappable
given.any('anything') // helper class with useful methods for any data type
Flooent objects only extend the native functionality, so you can still execute any native method like given.string('hello').includes('h')
.
To turn flooent objects back into their respective primitive form, use the valueOf()
method.
given.string('hello').valueOf()
When newing up a flooent object, you can also provide a callback as the second argument which will automatically turn the object back into its primitive form.
const shuffledNumbersRaw = given.array([1, 2, 3, 4], numbers => {
return numbers.shuffle()
})
After performing your data manipulations, if you need to use this data further, turn it back into its primitive form (see above) instead of passing it as an argument to another function or returning it.
This is to avoid cases such as flooent having a method (e.g. array.at) that later gets added to native JavaScript with different behaviour. That other function is not expecting a flooent object (specifically a third-party lib) and could make use of the at
method.
The contraints that apply to flooent strings and numbers are the same that apply to when you new up a native string/number using new (new String('')
) and is just how JavaScript works.
For one, the type will be object
instead of string
.
typeof given.string('') // object
typeof '' // string
Flooent strings and numbers are immutable. You can still do things like this:
given.string('?') + '!' // '?!'
given.number(1) + 1 // 2
which will return a primitive (not an instance of flooent).
However you can not mutate flooent objects like this:
given.string('') += '!' // ERROR
given.number(1) += 1 // ERROR
There are various fluent alternatives available.
If you only need to do a single thing, you can also import most functions individually. The result of these functions will not be turned into a flooent object.
import { afterLast } from 'flooent/string'
afterLast('www.example.com', '.') // 'com'
import { move } from 'flooent/array'
move(['music', 'tech', 'sports'], 0, 'after', 1) // ['tech', 'music', 'sports']
import { times } from 'flooent/number'
times(3, i => i) // [0, 1, 2]
import { rename } from 'flooent/map'
rename(new Map([['item_id', 1]]), 'item_id', 'itemId') // Map { itemId → 1 }
In addition, there is an experimental API for a pipable API:
import { pipe, afterLast, beforeLast, endWith, capitalize } from 'flooent/fp/string'
const path = 'App/Controllers/user.js'
pipe(path, afterLast('/'), beforeLast('.'), endWith('Controller'), capitalize) // UserController
Note: flooent/fp/string
, flooent/fp/map
, flooent/fp/number
, and flooent/fp/array
all return the same function pipe
.
You have access to everything from the native String object.
Executes the callback and transforms the result back into a flooent string if it is a string.
given.string('').pipe(str => str.append('!')) // String { '!' }
Compares the given value with the raw string.
given.string('flooent').is('flooent') // true
Checks if the string is included in the given array.
given.string('flooent').includedIn(['flooent', 'string'])
Returns the remaining text after the first occurrence of the given value. If the value does not exist in the string, the entire string is returned unchanged.
given.string('sub.domain.com').after('.') // String { 'domain.com' }
Returns the remaining text after the last occurrence of the given value. If the value does not exist in the string, the entire string is returned unchanged.
given.string('sub.domain.com').afterLast('.') // String { 'com' }
Returns the text before the first occurrence of the given value. If the value does not exist in the string, the entire string is returned unchanged.
given.string('sub.domain.com').before('.') // String { 'sub' }
Returns the text before the last occurrence of the given value. If the value does not exist in the string, the entire string is returned unchanged.
given.string('sub.domain.com').beforeLast('.') // String { 'sub.domain' }
Alias for concat
. Appends the given value to string.
given.string('hello').append(' world') // String { 'hello world' }
Prepends the given value to string.
given.string('world').prepend('hello ') // String { 'hello world' }
Appends the given value only if string doesn't already end with it.
given.string('hello').endWith(' world') // String { 'hello world' }
given.string('hello world').endWith(' world') // String { 'hello world' }
Prepends the given value only if string doesn't already start with it.
given.string('world').startWith('hello ') // String { 'hello world' }
given.string('hello world').startWith('hello ') // String { 'hello world' }
Truncates text to given length and appends second argument if string got truncated.
given.string('The quick brown fox jumps over the lazy dog').limit(9) // The quick...
given.string('The quick brown fox jumps over the lazy dog').limit(9, ' (Read more)') // The quick (Read more)
given.string('Hello').limit(10) // Hello
Tap into the chain without modifying the string.
given.string('')
.append('!')
.tap(str => console.log(str))
.append('!')
// ...
Executes the callback if first given value evaluates to true. Result will get transformed back into a flooent string if it is a raw string.
// can be a boolean
given.string('').when(true, str => str.append('!')) // String { '!' }
given.string('').when(false, str => str.append('!')) // String { '' }
// or a method
given.string('hello').when(str => str.endsWith('hello'), str => str.append(' world')) // String { 'hello world' }
given.string('hi').when(str => str.endsWith('hello'), str => str.append(' world')) // String { 'hello' }
Executes the callback if string is empty. Result will get transformed back into a flooent string if it is a raw string.
given.string('').whenEmpty(str => str.append('!')) // String { '!' }
given.string('hello').whenEmpty(str => str.append('!')) // String { 'hello' }
Wraps a string with the given value.
given.string('others').wrap('***') // String { '***others***' }
given.string('oldschool').wrap('<blink>', '</blink>') // String { '<blink>oldschool</blink>' }
Unwraps a string with the given value.
given.string('***others***').unwrap('***') // String { 'others' }
given.string('<blink>oldschool</blink>').unwrap('<blink>', '</blink>') // String { 'oldschool' }
Turns the string into camel case.
given('foo bar').camel() // String { 'fooBar' }
Turns the string into title case.
given.string('foo bar').title() // String { 'Foo Bar' }
Turns the string into studly case.
given('foo bar').studly() // String { 'FooBar' }
Capitalizes the first character.
given.string('foo bar').capitalize() // String { 'Foo bar' }
Turns the string into kebab case.
given('foo bar').kebab() // String { 'foo-bar' }
Turns the string into snake case.
given('foo bar').snake() // String { 'foo_bar' }
Turns the string into URI conform slug.
given.string('Foo Bar ♥').slug() // String { 'foo-bar' }
given.string('foo bär').slug('+') // String { 'foo+bar' }
Parses a string back into its original form.
given.string('true').parse() // true
given.string('23').parse() // 23
given.string('{\"a\":1}').parse() // { a: 1 }
You have access to everything from the native Array object.
Executes callback and transforms result back into a flooent array if the result is an array.
const someMethodToBePipedThrough = array => array.append(1)
given.array([]).pipe(someMethodToBePipedThrough) // [1]
Mutates the original array with the return value of the given callback. This is an escape hatch for when you need it and usually not recommended.
const numbers = given.array(1, 2, 3)
numbers.mutate(n => n.append(4)) // [1, 2, 3, 4]
numbers // [1, 2, 3, 4]
Returns the sum of the array.
given.array([2, 2, 1]).sum() // 5
See usage for arrays of objects.
Executes callback if first given value evaluates to true. Result will get transformed back into a flooent array if it is an array.
// can be a boolean
given.array([]).when(true, str => str.append(1)) // [1]
given.array([]).when(false, str => str.append(1)) // []
// or a method
given.array([]).when(array => array.length === 0), array => array.append('called!')) // ['called']
given.array([]).when(array => array.length === 1, array => array.append('called!')) // []
Returns a boolean whether the array is empty or not.
given.array([]).isEmpty() // true
given.array([1]).isEmpty() // false
Turns an array in the structure of [ ['key', 'value'] ]
into a flooent map.
given.map({ key: 'value' }).entries().toMap()
Filters array by given key / value pair.
const numbers = [1, 1, 2, 3]
given.array(numbers).where(1) // [1, 1]
See usage for arrays of objects.
Filters array by given values.
const numbers = [1, 1, 2, 3]
given.array(numbers).whereIn([1, 3]) // [1, 1, 3]
See usage for arrays of objects.
Removes given value from array.
const numbers = [1, 1, 2, 3]
given.array(numbers).whereNot(1) // [2, 3]
See usage for arrays of objects.
Removes given values from array.
const numbers = [1, 1, 2, 3]
given.array(numbers).whereNotIn([2, 3]) // [1, 1]
See usage for arrays of objects.
Returns the first (x) element(s) in the array or undefined.
given.array([1, 2, 3]).first() // 1
given.array([1, 2, 3]).first(2) // [1, 2]
Returns the second element in the array or undefined.
given.array([1, 2, 3]).second() // 2
Returns last (x) element(s) in array or undefined.
given.array([1, 2, 3]).last() // 3
given.array([1, 2, 3]).last(2) // [2, 3]
Alternatively, pass in a callback to get the last item that passes the given truth test (inverse of find
).
given.array([1, 2, 3]).last(item => item > 1) // 3
Returns element at given index or undefined. If given value is negative, it searches from behind.
given.array(['a', 'b', 'c']).nth(1) // 'b'
given.array(['a', 'b', 'c']).nth(5) // undefined
given.array(['a', 'b', 'c']).nth(-1) // 'c'
Return all items that don't pass the given truth test. Inverse of Array.filter
.
given.array([{ id: 1, disabled: true }]).reject(item => item.disabled) // []
Returns the items until either the given value is found, or the given callback returns true
.
given.array(['a', 'b', 'c']).until('c') // ['a', 'b']
given.array(['a', 'b', 'c']).until(item => item === 'c') // ['a', 'b']
Shuffles the array.
given.array([1, 2, 3]).shuffle() // ?, maybe: [1, 3, 2]
Returns array of unique values.
given.array([1, 1, 2]).unique() // [1, 2]
See usage for arrays of objects.
Breaks the array into multiple, smaller arrays of a given size.
given.array([1, 2, 3, 4, 5]).chunk(3) // [[1, 2, 3], [4, 5]]
Returns the items for the given page and size.
given.array(['a', 'b', 'c', 'd', 'e', 'f', 'g']).forPage(1, 3) // ['a', 'b', 'c']
given.array(['a', 'b', 'c', 'd', 'e', 'f', 'g']).forPage(2, 3) // ['d', 'e', 'f']
given.array(['a', 'b', 'c', 'd', 'e', 'f', 'g']).forPage(3, 3) // ['g']
given.array(['a', 'b', 'c', 'd', 'e', 'f', 'g']).forPage(4, 3) // []
Fills up the array with the given value.
given.array([1, 2, 3]).pad(5, 0) // [1, 2, 3, 0, 0]
Only returns items which are not empty.
given.array([0, '', null, undefined, 1, 2]).filled() // [1, 2]
See usage for arrays of objects.
Returns a tuple separating the items that pass the given truth test.
const users = given.array([{ id: 1, active: false }, { id: 2, active: false }, { id: 3, active: true }])
const [activeUsers, inactiveUsers] = users.partition(user => user.active)
Prepends the given items to the array. Unlike unshift
, it is immutable and returns a new array.
const numbers = given.array([2, 3])
numbers.prepend(0, 1) // [0, 1, 2, 3]
To prepend items at a specific index, check out the Pointer API.
Appends the given items to the array. Unlike push
, it is immutable and returns a new array.
const numbers = given.array([0, 1])
numbers.append(2, 3) // [0, 1, 2, 3]
To append items at a specific index, check out the Pointer API.
Sorts an array in their respective order and returns a new array.
given.array([3, 1, 2]).sortAsc() // [1, 2, 3]
given.array([3, 1, 2]).sortDesc() // [3, 2, 1]
See usage for arrays of objects.
Tap into the chain without modifying the array.
given.array([])
.append(1)
.tap(array => console.log(array))
.append(2)
// ...
Points to a specific index inside the array to do further actions on it.
given.array(['music', 'video', 'tech']).point(1) // returns pointer pointing to 'video'
given.array(['music', 'video', 'tech']).point(-1) // returns pointer pointing to 'tech'
given.array(['music', 'video', 'tech']).point(item => item === 'music') // returns pointer pointing to 'music'
Appends given value to array in between the currently pointed item and its next item and returns a new array.
given.array(['music', 'tech']).point(0).append('video') // ['music', 'video', 'tech']
Prepends given value to array in between the currently pointed item and its previous item and returns a new array.
given.array(['music', 'tech']).point(1).prepend('video') // ['music', 'video', 'tech']
Sets the value at the current index and returns a new array.
given.array(['music', 'tec']).point(1).set(item => item + 'h') // ['music', 'tech']
Removes the current index and returns a new array.
given.array(['music', 'tech']).point(1).remove() // ['music']
Splits the array at the current index
given.array(['a', 'is', 'c']).point(1).split() // [['a'], ['c']]
Returns the value for current pointer position.
given.array(['music', 'tech']).point(1).value() // ['music', 'tech']
Steps forward or backwards given the number of steps.
given.array(['music', 'tec']).point(1).step(-1).value() // ['music']
Moves an item in the array using the given source index to either "before" or "after" the given target.
given.array(['b', 'a', 'c']).move(0, 'after', 1) // ['a', 'b', 'c']
given.array(['b', 'a', 'c']).move(0, 'before', 2) // ['a', 'b', 'c']
given.array(['b', 'a', 'c']).move(1, 'before', 0) // ['a', 'b', 'c']
Instead of the index, you can also specify "first" or "last":
given.array(['c', 'a', 'b']).move('first', 'after', 'last') // ['a', 'b', 'c']
given.array(['b', 'c', 'a']).move('last', 'before', 'first') // ['a', 'b', 'c']
Returns the sum of the given field/result of callback in the array.
const users = [{ id: 1, points: 10 }, { id: 2, points: 10 }, { id: 3, points: 10 }]
given.array(users).sum('points') // 30
given.array(users).sum(user => user.points * 10) // 300
Sorts an array in their respective order and returns a new array.
const numbers = [{ val: 3 }, { val: 1 }, { val: 2 }]
given.array(numbers).sortAsc('val') // [{ val: 1 }, { val: 2 }, { val: 3 }]
given.array(numbers).sortDesc('val') // [{ val: 3 }, { val: 2 }, { val: 1 }]
Also works by passing the index (useful when working with array entries).
given.array([[0], [2], [1]]).sortAsc(0)) // [[0], [1], [2]])
Alternatively, pass in a map function of which its result will become the key instead.
const numbers = [{ val: 3 }, { val: 1 }, { val: 2 }]
given.array(numbers).sortAsc(item => item.val) // [{ val: 1 }, { val: 2 }, { val: 3 }]
given.array(numbers).sortDesc(item => item.val) // [{ val: 3 }, { val: 2 }, { val: 1 }]
Pluck the given field out of each object in the array.
const cities = [
{ id: 1, name: 'Munich' },
{ id: 2, name: 'Naha' },
]
given.array(cities).pluck('name') // ['Munich', 'Naha']
Filters array by given key / value pair.
const cities = [
{ id: 1, name: 'Munich' },
{ id: 2, name: 'Naha' },
{ id: 3, name: 'Naha' },
]
given.array(cities).where('name', 'Munich') // [{ id: 1, name: 'Munich' }]
Removes items from array by the given key / value pair.
const cities = [
{ id: 1, name: 'Munich' },
{ id: 2, name: 'Naha' },
{ id: 3, name: 'Naha' },
]
given.array(cities).whereNot('name', 'Naha') // [{ id: 1, name: 'Munich' }]
Filters array by given key and values.
const cities = [
{ id: 1, name: 'Munich' },
{ id: 2, name: 'Naha' },
{ id: 3, name: 'Yoron' },
]
given.array(cities).whereIn('name', ['Munich', 'Yoron']) // [{ id: 1, name: 'Munich' }, { id: 3, name: 'Yoron' }]
Removes items from array by the given key and values.
const cities = [
{ id: 1, name: 'Munich' },
{ id: 2, name: 'Naha' },
{ id: 3, name: 'Yoron' },
]
given.array(cities).whereNotIn('name', ['Naha', 'Yoron']) // [{ id: 1, name: 'Munich' }]
Omits given keys from all objects in the array.
const people = [
{ id: 1, age: 24, initials: 'mz' },
{ id: 2, age: 2, initials: 'lz' }
]
given.array(people).omit(['initials', 'age']) // [ { id: 1 }, { id: 2 } ])
Returns array of unique values comparing the given key.
const items = [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }, { id: 1, name: 'music' }]
given.array(items).unique('id') // [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }]
Alternatively, pass in a function of which its result will become the key instead.
const items = [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }, { id: 3, name: 'MUSIC' }]
given.array(items).unique(item => item.name.toLowerCase()) // [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }]
Only returns items which are not empty.
const items = [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }, { id: 3, name: '' }]
given.array(items).filled('name') // [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }]
Groups an array by the given key and returns a flooent map.
const items = [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }, { id: 3, name: 'music' }]
given.array(items).groupBy('name') // result is:
/*
{
music: [{ id: 1, name: 'music' }, { id: 3, name: 'music' }],
movie: [{ id: 2, name: 'movie' }]
}
*/
Alternatively, pass in a function of which its result will become the key instead.
const items = [{ id: 1, name: 'Music' }, { id: 2, name: 'movie' }, { id: 3, name: 'music' }]
given.array(items).groupBy(item => item.name.toUpperCase()) // result is:
/*
{
MUSIC: [{ id: 1, name: 'music' }, { id: 3, name: 'music' }],
MOVIE: [{ id: 2, name: 'movie' }]
}
*/
Keys the collection by the given key and returns a flooent map. If multiple items have the same key, only the last one will appear in the new collection.
const items = [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }, { id: 3, name: 'music' }]
given.array(items).keyBy('name') // result is:
/*
{
music: { id: 3, name: 'music' },
movie: { id: 2, name: 'movie' }
}
*/
Turns the given array into a flooent map with each element becoming a key in the map.
const genres = ['music', 'tech', 'games']
const map = given.array(genres).toKeyedMap(null) // result is:
/*
{
music: null,
tech: null,
games: null
}
*/
Alternatively, pass in a callback to specify the default value for each item individually:
const genres = ['music', 'tech', 'games']
const map = given.array(genres).toKeyedMap(genre => genre.toUpperCase()) // result is:
/*
{
music: 'MUSIC',
tech: 'TECH',
games: 'GAMES'
}
*/
You have access to everything from the native Map object.
The native methods keys()
, entries()
and values()
will return an instance of flooent Array instead of a native Array.
For nested data structures, only the first layer gets transformed into a map
Turns the map into an object.
const map = given.map({ key: 'value' }) // Map { key → "value" }
map.toObject() // { key: 'value' }
map.toJSON() // { key: 'value' }
Returns the value for the given key and deletes the key value pair from the map (mutation).
const map = given.map({ key: 'value' })
map.pull('key') // 'value'
map.has('key') // false
Iterates the entries through the given callback and assigns each result as the key.
const map = given.map({ a: 1 }).mapKeys((value, key, index) => key + value)
map.get('a1') // 1
Iterates the entries through the given callback and assigns each result as the value.
const map = given.map({ a: '1' }).mapValues((value, key, index) => key + value)
map.get('a') // a1
Returns a new map with only the given keys.
given.map({ one: 1, two: 2, three: 3 }).only(['one', 'two']) // Map { "one" → 1, "two" → 2 }
Inverse of only
. Returns a new map with all keys except for the given keys.
given.map({ one: 1, two: 2, three: 3 }).except(['one', 'two']) // Map { "three" → 3 }
Rearranges the map to the given keys. Any unmentioned keys will be appended to the end.
given.map({ strings: 2, numbers: 1, functions: 4 })
.arrange('numbers', 'functions')
.keys() // ['numbers', 'functions', 'strings']
Renames the given key with the new key if found, keeping the original insertion order.
given.map({ one: 1, to: 2, three: 3 })
.rename('to', 'two')
.keys() // ['one', 'two', 'three']
You have access to everything from the native Number object.
Executes the callback for number of base values' times and returns a flooent array with the result of each iteration.
given.number(3).times(i => i) // [0, 1, 2]
Fills up the number with zeroes.
given.number(40).pad(4) // '0040'
Returns the number with its ordinal suffix. Only supports English.
given.number(1).ordinal() // '1st'
given.number(9).ordinal() // '9th'
Checks if the number is between two given numbers. isBetweenOr
is inclusive, while isBetween
is exclusive.
given.number(5).isBetween(1, 10) // true
given.number(5).isBetween(5, 10) // false
given.number(5).isBetweenOr(5, 10) // true
given.number(40).percent().of(750) // Number { 300 }
given.number(300).of(750).inPercent() // Number { 40 }
Rounds down until .4 and up from .5.
given.number(10.4).round() // Number { 10 }
given.number(10.5).round() // Number { 11 }
Always rounds its value up to the next largest whole number or integer.
given.number(10.2).ceil() // Number { 11 }
Always rounds its value down.
given.number(10.9).floor() // Number { 10 }
A generic helper class for any kind of data types.
Executes and returns the result of a callback.
This is useful for grouping common logic together and avoiding temporary variables.
Before
const user = User.first() // variable "user" is only used here
const nameMatches = expect(user.name).toBe('test name')
After
const nameMatches = given.any(User.first()).do(user => {
return expect(user.name).toBe('test name')
})
Extending flooent methods is easy as pie thanks to macro
.
import { given } from 'flooent'
given.string.macro('scream', function() {
return this.toUpperCase()
})
given.string('hello').scream() // String { 'HELLO' }
Define macros at a central place before your business logic. E.g. entry point or service provider
For TypeScript support, you need to additionally declare the module.
declare module 'flooent' {
interface Stringable {
scream(): Stringable;
}
}
These methods, while convenient, are not in the core since they are not all too common yet quadruply the bundle size among other reasons.
Array.is
Deep compares an array with the given callback.import { given } from 'flooent'
import isequal from 'lodash.isequal' // npm install lodash.isequal
given.array.macro('is', function(compareWith) {
return isequal(this, compareWith)
})
Then, use it like this:
const users = [{ id: 1 }]
given.array(users).is([{ id: 1 }]) // true
Array.clone
Deep clone an array and map.import { given } from 'flooent'
import clonedeep from 'lodash.clonedeep' // npm install lodash.clonedeep
given.array.macro('clone', function() {
// lodash does array.constructor(length) which doesn't work on subclassed arrays
const clone = clonedeep([...this])
return this.constructor.from(clone)
})
given.map.macro('clone', function() {
return this.entries().clone().toMap()
})
Then, use it like this:
given.array([['key', 'value']]).clone()
given.map([['key', 'value']]).clone()
String.plural & String.singular
Turns string into plural/singular form.import { given } from 'flooent'
import pluralize from 'pluralize' // npm install pluralize
given.string.macro('plural', function(count) {
const plural = pluralize(this, count, false)
return new this.constructor(plural) // new up again because pluralize returns raw string.
})
given.string.macro('singular', function() {
return new this.constructor(pluralize.singular(this))
})
Then, use it like this:
given.string('child').plural() // String { 'children' }
given.string('child').plural(3) // String { 'children' }
given.string('child').plural(1) // String { 'child' }
given.string('children').singular() // String { 'child' }
given.string('child').singular() // String { 'child' }
- Drop CJS once ES modules are widely supported in Node. ES modules are much lighter.