flooent v3.0.0
flooent
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()
Index
Get Started
Installation
npm install flooent
given
Use given
to create either a flooent Array, Map or String.
import { given } from 'flooent'
given.string('hello') // instance of Stringable
given.array([1, 2]) // instance of Arrayable
given.map(new Map) // or given.map([['key', 'value']]) | instance of Mappable
given.map.fromObject({ key: 'value' }) // instance of Mappable
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()
Best Practices
After transforming your data, convert the object back to its primitive form before passing it to another function or returning it.
This is to avoid naming collisions with possibly new native methods:
import { given } from 'flooent'
import { arrayToCsv } from 'some-csv-lib'
// Avoid this
const sortedItems = given.array(items).sortAsc('id')
arrayToCsv(sortedItems)
// instead, do this
const sortedItems = given.array(items).sortAsc('id').valueOf()
arrayToCsv(sortedItems)
Constraints
The contraints that apply to flooent strings are the same that apply to when you new up a native string 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 are immutable. You can still do things like this:
given.string('?') + '!' // '?!'
which will return a primitive (not an instance of flooent).
However you can not mutate flooent objects like this:
given.string('') += '!' // ERROR
There are various fluent alternatives available.
Functional API
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 { rename } from 'flooent/map'
rename(new Map([['item_id', 1]]), 'item_id', 'itemId') // Map { itemId → 1 }
import { rename } from 'flooent/object'
rename({ item_id: 1 }), 'item_id', 'itemId') // { itemId: 1 }
Strings
You have access to everything from the native String object.
Fluent methods
pipe
Executes the callback and transforms the result back into a flooent string if it is a string. Useful for creating reusable functions for specific method combinations, or for continuing the chain when using non-flooent functions.
const append = str => str.append('!').prepend('!!')
given.string('').pipe(append) // String { '!' }
after
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' }
afterLast
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' }
before
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' }
beforeLast
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' }
append
Alias for concat
. Appends the given value to string.
given.string('hello').append(' world') // String { 'hello world' }
prepend
Prepends the given value to string.
given.string('world').prepend('hello ') // String { 'hello world' }
endWith
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' }
startWith
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' }
limit
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
Tap into the chain without modifying the string.
given.string('')
.append('!')
.tap(str => console.log(str))
.append('!')
// ...
when
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' }
whenEmpty
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' }
wrap
Wraps a string with the given value.
given.string('others').wrap('***') // String { '***others***' }
given.string('oldschool').wrap('<blink>', '</blink>') // String { '<blink>oldschool</blink>' }
unwrap
Unwraps a string with the given value.
given.string('***others***').unwrap('***') // String { 'others' }
given.string('<blink>oldschool</blink>').unwrap('<blink>', '</blink>') // String { 'oldschool' }
camel
Turns the string into camel case.
given('foo bar').camel() // String { 'fooBar' }
title
Turns the string into title case.
given.string('foo bar').title() // String { 'Foo Bar' }
studly
Turns the string into studly case.
given('foo bar').studly() // String { 'FooBar' }
capitalize
Capitalizes the first character.
given.string('foo bar').capitalize() // String { 'Foo bar' }
kebab
Turns the string into kebab case.
given('foo bar').kebab() // String { 'foo-bar' }
snake
Turns the string into snake case.
given('foo bar').snake() // String { 'foo_bar' }
slug
Turns the string into URI conform slug.
given.string('Foo Bar ♥').slug() // String { 'foo-bar' }
given.string('foo bär').slug('+') // String { 'foo+bar' }
Arrays
You have access to everything from the native Array object.
Non-Fluent methods
sum
Returns the sum of the array.
given.array([2, 2, 1]).sum() // 5
See usage for arrays of objects.
toMap
Turns an array in the structure of [ ['key', 'value'] ]
into a flooent map.
const entries = [['key', 'value']]
given.array(entries).toMap()// FlooentMap { itemId → 1 }
sized
Creates a flooent array of the specified length and populates it using the callback function.
given.array.sized(i => i) // [0, 1, 2]
Fluent methods
pipe
Executes callback and transforms result back into a flooent array if the result is an array. Useful for creating reusable functions for specific method combinations, or for continuing the chain when using non-flooent functions.
const reusableFunction = array => array.append(1)
given.array([]).pipe(reusableFunction) // [1]
mutate
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]
when
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!')) // []
where
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.
whereIn
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.
whereNot
Removes given value from array.
const numbers = [1, 1, 2, 3]
given.array(numbers).whereNot(1) // [2, 3]
See usage for arrays of objects.
whereNotIn
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.
reject
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) // []
until
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']
shuffle
Shuffles the array.
given.array([1, 2, 3]).shuffle() // ?, maybe: [1, 3, 2]
unique
Returns array of unique values.
given.array([1, 1, 2]).unique() // [1, 2]
See usage for arrays of objects.
chunk
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]]
forPage
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) // []
pad
Fills up the array with the given value.
given.array([1, 2, 3]).pad(5, 0) // [1, 2, 3, 0, 0]
filled
Only returns items which are not empty.
given.array([0, '', null, undefined, 1, 2]).filled() // [1, 2]
See usage for arrays of objects.
partition
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)
prepend
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.
append
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.
sortAsc / sortDesc
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
Tap into the chain without modifying the array.
given.array([])
.append(1)
.tap(array => console.log(array))
.append(2)
// ...
Pointer API
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'
append
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']
prepend
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']
set
Sets the value at the current index and returns a new array.
given.array(['music', 'tec']).point(1).set(item => item + 'h') // ['music', 'tech']
remove
Removes the current index and returns a new array.
given.array(['music', 'tech']).point(1).remove() // ['music']
split
Splits the array at the current index
given.array(['a', 'is', 'c']).point(1).split() // [['a'], ['c']]
value
Returns the value for current pointer position.
given.array(['music', 'tech']).point(1).value() // ['music', 'tech']
step
Steps forward or backwards given the number of steps.
given.array(['music', 'tec']).point(1).step(-1).value() // ['music']
move
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']
Methods for arrays of objects
sum
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
sortAsc / sortDesc
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
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']
where
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' }]
whereNot
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' }]
whereIn
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' }]
whereNotIn
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' }]
omit
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 } ])
unique
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' }]
filled
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' }]
groupBy
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').toObject() // 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()).toObject() // result is:
/*
{
MUSIC: [{ id: 1, name: 'music' }, { id: 3, name: 'music' }],
MOVIE: [{ id: 2, name: 'movie' }]
}
*/
There is no standalone function for "groupBy". Instead, use the native "Map.groupBy" or "Object.groupBy" (they only support a callback as the argument).
keyBy
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').toObject() // result is:
/*
{
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).keyBy(item => item.name).toObject()
toKeyedMap
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).toObject() // 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()).toObject() // result is:
/*
{
music: 'MUSIC',
tech: 'TECH',
games: 'GAMES'
}
*/
Maps
You have access to everything from the native Map object.
You construct a flooent map the same way as a native map:
// using an array in the format of entries
given.map([['key', 'value']])
// using an existing map
given.map(new Map())
Additionally, since normal objects don't have a fluent API in general, you can turn your objects into a map, perform any manipulations and turn them back into an object instead:
given.map
.fromObject({ key: 'value' })
.rename('key', 'id')
.toObject()
For nested data structures, only the first layer gets transformed into a map
Using standalone functions, there are two variants:
import { mapKeys } from 'flooent/map' // -> working with maps
import { mapKeys } from 'flooent/object' // -> working with objects
toEntries, toKeys, toValues
flooent variants of the native methods entries
, keys
, and values
.
Instead of a MapIterator, these return a flooent array instead.
const map = given.map.fromObject({ key: 'value' })
map.toKeys() // ['key']
map.toValues() // ['value']
map.toEntries() // [['key', 'value']]
pipe
Executes the callback and transforms the result back into a flooent map if it is a map. Useful for creating reusable functions for specific method combinations, or for continuing the chain when using non-flooent functions.
const extractAreas = map => map.only(['area1', 'area2', 'area3'])
given.map(cityMap).pipe(extractAreas) // String { '!' }
when
Executes the callback if first given value evaluates to true. Result will get transformed back into a flooent map if it is a raw map.
// can be a boolean
given.map(cityMap).when(true, map => map.set('id', genId()))
// or a method
given.map(cityMap).when(map => map.has('isNew'), map => map.set('id', genId()))
pull
Returns the value for the given key and deletes the key value pair from the map (mutation).
const map = given.map.fromObject({ key: 'value' })
map.pull('key') // 'value'
map.has('key') // false
mapKeys
Iterates the entries through the given callback and assigns each result as the key.
const map = given.map.fromObject({ a: 1 }).mapKeys((value, key, index) => key + value)
map.get('a1') // 1
mapValues
Iterates the entries through the given callback and assigns each result as the value.
const map = given.map.fromObject({ a: '1' }).mapValues((value, key, index) => key + value)
map.get('a') // a1
only
Returns a new map with only the given keys.
given.map.fromObject({ one: 1, two: 2, three: 3 }).only(['one', 'two']) // Map { "one" → 1, "two" → 2 }
except
Inverse of only
. Returns a new map with all keys except for the given keys.
given.map.fromObject({ one: 1, two: 2, three: 3 }).except(['one', 'two']) // Map { "three" → 3 }
arrange
Rearranges the map to the given keys. Any unmentioned keys will be appended to the end.
given.map.fromObject({ strings: 2, numbers: 1, functions: 4 })
.arrange('numbers', 'functions')
.toKeys() // ['numbers', 'functions', 'strings']
rename
Renames the given key with the new key if found, keeping the original insertion order.
given.map.fromObject({ one: 1, to: 2, three: 3 })
.rename('to', 'two')
.toKeys() // ['one', 'two', 'three']
Macros (Extending flooent)
Extend flooent with your own custom methods using 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
TypeScript
For TypeScript support, you need to additionally declare the module.
declare module 'flooent' {
interface Stringable { // Stringable | Arrayable | Mappable
scream(): Stringable;
}
}
More examples
These methods, while convenient, are not in the core since they are not all too common yet quadruply the bundle size among other reasons.
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
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.toEntries().clone().toMap()
})
Then, use it like this:
given.array([['key', 'value']]).clone()
given.map([['key', 'value']]).clone()
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' }
4 months ago
5 months ago
12 months ago
2 years ago
2 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago