flat2 v1.3.0
flat2
Flatten a nested Javascript object any way you like. Designed to be highly customisable
Intro
This library is a fully rewritten version of flat.
flat2 has been designed using ES6 classes to make for a more composable, flexible and more maintainable library.
It should now be much easier to test, debug and extend. It is designed to be way more customisable and flexible than the original flat. Please also consider the alternative libs listed at the end of this Readme.
Status
All tests pass except for typed arrays which is rarely used in any case.
I welcome a PR to fix this little issue.
Installation
$ npm install flat2 --saveMethods
flatten(obj, opts)
Creates a flattener, flattens the object and returns the result.
const { flatten } = require('flat2')
const flatObj = flatten({
key1: {
keyA: 'valueI'
},
key2: {
keyB: 'valueII'
},
key3: { a: { b: { c: 2 } } }
})
// {
// 'key1.keyA': 'valueI',
// 'key2.keyB': 'valueII',
// 'key3.a.b.c': 2
// }By default uses . as the delimiter. The key generation can be customised in many ways by passing options.
Flattener
Instead of using the convenience flatten function, you can also option to instantiate a Flattener instance for reuse, to flatten multiple objects.
The flattener is reset at the start of each flatten operation.
createFlattener(obj, options)
Returns a flattener instance.
const { createFlattener } = require('flat2')
const flattener = createFlattener(obj1, opts)Use flattener.flat(obj) to flatten objects
// flatten using initial target
const flatObj1 = flattener.flat()
// reuse flattener and flatten with new target
const flatObj2 = flattener.flat(obj2)You can also import the Flattener class and use new or extend it to create your own custom Flattener
import {
Flattener
} from 'flat2'
class MyFlattener extends Flattener {
// custom overrides/extensions
}
const flattener = new MyFlattener(obj, opts)
/// use it!
const flatObj = flattener.flatten()Options
The following are the supported options, ie. second argument to flatten function (or Flattener constructor)
delimiter
Use a custom delimiter for (un)flattening your objects, instead of ..
flatten(obj, {
delimiter: ':'
}Transform options
toUpperCase
Use toUpperCase option for flattening your objects and upper case object keys at the same time. This can be handy when working with constants, i.e. API_KEY: 'some key'. This is a built in key transform.
flatten(obj, {
toUpperCase: true
}toLowerCase
Use a toLowerCase option for flattening your objects and lower case object keys at the same time. This is a built in key transform.
flatten(obj, {
toLowerCase: true
}keyType
Use a built in type of key transformation function:
snakeCase(ie.ale_beta_zeta)camelCase(ie.aleBetaZeta)
flatten(obj, {
keyType: 'snakeCase'
}Advanced Configuration options
You can use the following options to further customize how keys are generated
transformKeyFn
Use a custom function to transform object keys (as opposed to built in transforms such as toLowerCase and toUpperCase). A transform is applied after the key name has been generated.
flatten(obj, {
transformKeyFn: (key) => mySpecialTransform(key)
}keyNameFn
Use a custom function to flatten the keyname. By default, the delimiter is inserted between prev and next
Here's an example that uses a colon (:) to both prefix and delimit the keyname
var obj = {
hello: {
world: {
again: 'good morning'
}}}
flatten(obj, { keyNameFn: function(prev, next) {
return prev
? prev + ':' + next // delimit
: ':' + next // prefix
}})
// {
// ':hello:world:again': 'good morning'
// }createKeyNameFn
Use a custom factory function to create the keyname function.
const {
myDefaultKeyNameFn,
createMySpecialKeyNameFn
} = './key-functions'
flatten(obj, {
createKeyNameFn(opts) {
return opts.special ? createMySpecialKeyNameFn(opts) : myDefaultKeyNameFn
}
})createFlatKeyFn
Use createFlatKeyFn to pass a factory that generates a function which implements the FlatKey interface (see FlatKeyClass below)
const {
myFlatKey,
} = './key-functions'
flatten(obj, {
createFlatKeyFn(opts) {
return function (opts) {
return {
config(prev, next) {
// ...
}
get name() {
//
}
}
}
}
})FlatKeyClass
Use a custom FlatKey class. Must implement the following interface:
config(key, prev)namegetter
It is used by the default keyNameFn to generate each keyname as follows
return (key, prev) => {
return flatKey.config(key, prev).name
}Internal Flow configuration
In case you want to customize the internal flow logic:
createStepper
To pass a custom createStepper function to override the built-in createStepper factory. You can f.ex extend the Stepper class with your customisations.
function createStepper(object, {
flattener,
prev,
currentDepth
}) {
return new MyStepper(object, {
flattener,
prev,
currentDepth
})
}
flatten(obj, {
createStepper
})createKeyStepper
To pass a custom createKeyStepper function to override the built-in createKeyStepper factory. You can f.ex extend the KeyStepper class with your customisations.
function createKeyStepper(key, { stepper, flattener }) {
return new MyKeyStepper(key, { stepper, flattener })
}
flatten(obj, {
createKeyStepper
})Customized output
transformValue
Pass a transformValue function as option to fine-tune if/how leaf values are transformed. By default the value is returned, but you have access to loads of information to fine-tune if necessary.
function transformValue(value, {
target,
key,
newKey,
prevKey,
ancestorKeys,
lvKeys
}) {
return value
}Modes
You can enable safe mode to preserve values such as arrays.
safe
When enabled flat will preserve arrays and their
contents. This is disabled by default.
var flatten = require('flat')
flatten({
this: [
{ contains: 'arrays' },
{ preserving: {
them: 'for you'
}}
]
}, {
safe: true
})
// {
// 'this': [
// { contains: 'arrays' },
// { preserving: {
// them: 'for you'
// }}
// ]
// }Control flow options
maxDepth
Maximum number of nested objects to flatten.
var flatten = require('flat')
flatten({
key1: {
keyA: 'valueI'
},
key2: {
keyB: 'valueII'
},
key3: { a: { b: { c: 2 } } }
}, { maxDepth: 2 })
// {
// 'key1.keyA': 'valueI',
// 'key2.keyB': 'valueII',
// 'key3.a': { b: { c: 2 } }
// }filter
Decide if a value should be flattened any further
var flatten = require('flat')
flatten({
key1: {
keyA: 'valueI'
},
key2: {
keyB: 'valueII'
}
}, { filter: (value) => !value.keyA }) // skip key1
// {
// key1: {
// keyA: 'valueI'
// },
// 'key2.keyB': 'valueII'
// }Event handlers
The Flattener and Stepper classes comes with built-in event handlers to maintain stacks of visited nodes. You can override these event handler to suit your own needs.
Flattener callbacks
onKey(key, depth)onStepsDone(depth)
These events are used to build the stack of ancestorKeys visited while traversing a branch depth first.
Stepper callbacks
onKey(key)
A stepper steps through all keys on one depth level.
The onKey handler of stepper is used to build a stack of visited keys on a particular depth level. It also calls onKey for the flattener to allow it to build up its stack.
Logging
You can pass a boolean logging option to enable/disable logging.
Add a logOnly option to limit which "classes" should log. A class is any object that has a name property (note: name will default to constructor.name).
flatten({
HELLO: {
WORLD: {
AGAIN: 'good morning'
}
}
}, {
toLowerCase: true,
logging: true,
logOnly: ['Flattener']
})Currently you can supply a logWhen option that takes an object which can be an instance of either:
FlattenerStepperKeyStepperFlatKey
All of these instances should have access to flattener where you have access to attributes such as:
currentDepthlastAncestorKeyancestorKeyslvKeys(via currentstepperattached)
You can use these attributes to further control when to log.
See TODO.md for our Logger plans.
logger option
You can pass your own custom logger in the new logger option.
The custom logger can implement one or more of the following:
log(...args)objLog(...args)
The default Logger methods:
log(...args) {
this.shouldLog && logInfo(this.name, ...args)
}
objLog(...args) {
this.shouldLog && logObj(this.name, ...args)
}Your custom logger can be an instance of your custom logger class that extends Logger (also exported for convenience)
Example:
import {
Logger,
// ...
} from 'flat2`
class MyLogger extends Logger {
constructor(flattener, opts) {
super(opts)
this.flattener = flattener
}
get shouldLog() {
// custom conditions
}
}
const flattener = createFlattener(obj, opts)
const loggingOpts = {
logging: true,
// ... custom opts ?
}
const logger = new MyLogger(flattener, loggingOpts)
flattener.logger = logger
// now ready to use flattener with new logger!TODO: Improved logger and composition
We would welcome a PR with more detailed logging constraints, such as current depth level, keys visited etc. and using composition instead of deep inheritance
Publish/Subscribe
It might sometimes be useful to have a way to post generate the output values after the fact using the information available at the time of generation for each flat key.
This can f.ex be useful for mapping values (pointers) from the nested to the flat structure.
You can now pass a subscribeValue callback, which can be called after the fact by the flattener.
import {
leaf,
// ... more refs imported
} from 'flat2'
function subscribeValue(newKey, newValue, {
target,
ancestorKeys
})
leaf(target, ancestorKeys.join('.'), newValue)
}The function leaf is also included and exported.
function leaf(obj, path, value) {
const pList = path.split('.');
const key = pList.pop();
const pointer = pList.reduce((accumulator, currentValue) => {
if (accumulator[currentValue] === undefined) accumulator[currentValue] = {};
return accumulator[currentValue];
}, obj);
pointer[key] = value;
return obj;
}This way you can map the deeply nested leaf values of a target object to point to leaf values in an obj with the flat key structure.
// assuming flatStruct has structure mirroring object of returned by flatten
const styles = createStyleSheet(flatObj)
const nestedStyles = {}
flattener.publishObj(styles, target)Now nestedStyles.card.container will point to styles.cardContainer and so on ;)
Note: You can also publish key/value pair individually using flattener.publish(key, value, target)
See also: Safely accessing deeply nested values
Alternatives
There are a host of alternative solutions you might want to consider...