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 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 MappableFlooent 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()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)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 '' // stringFlooent 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('') += '!' // ERRORThere 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 { 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 }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. 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 { '!' }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) // HelloTap 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' }You have access to everything from the native Array object.
Returns the sum of the array.
given.array([2, 2, 1]).sum() // 5See usage for arrays of objects.
Turns an array in the structure of [ ['key', 'value'] ] into a flooent map.
const entries = [['key', 'value']]
given.array(entries).toMap()// FlooentMap { itemId → 1 }Creates a flooent array of the specified length and populates it using the callback function.
given.array.sized(i => i) // [0, 1, 2]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]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]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!')) // []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.
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) // 300Sorts 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').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).
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()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'
}
*/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 objectsflooent 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']]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 { '!' }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()))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') // falseIterates 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') // 1Iterates 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') // a1Returns 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 }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 }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']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']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
For TypeScript support, you need to additionally declare the module.
declare module 'flooent' {
interface Stringable { // Stringable | Arrayable | Mappable
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 }]) // trueArray.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.toEntries().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' }If the call-this proposal ever gets added to ES, it would eliminate all constraints and complexities.
Current Implementation:
given.string(path)
.afterLast('/')
.beforeLast('.')
.endWith('Controller')
.toLowerCase()
.valueOf()Potential Future Implementation:
path
::afterLast('/')
::beforeLast('.')
::endWith('Controller')
.toLowerCase()