Elastic Object

DeepScan grade

Plain Objects are great for handling data, but they can be a bit clunky. This is where Elastic Object comes in. On top of the regular object functionality, it features get() and set() with dotted.string.notation, as well as loops and all the other good stuff you already know from Arrays, Maps or Sets. It is easily extendable in case you want to add your own methods. And it's not huge either.

Installation

npm i elastic-object

Usage

Below is a simple example of how to create an Elastic Object. For the methods, please refer to the complete documentation.

import ElasticObject from 'elastic-object'; // note that ElasticObject is implemented as ESM and not in CJS

const eObj = new ElasticObject({
    path: {
        to: {
            string: "foo",
            integer: 42
        },
    },
    another: {
        path: {
            to: {
                float: 3.14
            }
        }
    }
});

console.log(eObj.get('path.to.string')); // "foo"
eObj.set('path.to.string', "bar");
console.log(eObj.get('path.to.string')); // "bar"
eObj.path.to.string = "quux";
console.log(eObj.get('path.to.string')); // "quux"

eObj.forEach((value, key) => {
    console.log(key, value); // "path", { to: { string: "bar", integer: 42 } } etc.
});

Features

Standard object methods

Elastic Objects are extensions of plain objects, so everything you can do with plain objects can be done with elastic objects, too. There are some differences, though:

  • Static methods, such as assign() or create(), which you would expect to return regular objects, will return elastic objects instead.
  • keys(), values(), entries(), assign(), create() and fromEntries() are also available as instance methods. From within the instance keys(), values(), entries() refer to this and take no arguments. assign() uses this as the target argument. create() and fromEntries() work exactly like their static counterparts.

Accessors

Accessing properties with set('path.to.property') is a common implementation pattern, but it's not native to JavaScript. With set(), get(), has() and unset() Elastic Object has built-in support for this pattern. To avoid confusion with JavaScript's native dot notation, this document uses the term dotted.string.notation instead. The feature is powered by Sindre Sorhus' dot-prop library.

Array methods

Not all array methods make sense on an object, but every(), filter(), find(), forEach(), includes(), length(), map(), reduce(), reduceRight(), some() and sort() certainly do; Elastic Object has them all. There is also findPath() as an equivalent of findIndex(). All methods work pretty much like their array counterparts, which means they generally refer to the top level of the object. findPath(), however, searches all levels as long as the values are either arrays or objects. Note that length() is, contrary to the array implementation, built out as a function.

Other methods

toJson(), clone() and cloneProperty() cover common tasks and it makes sense to have them available. The flatten() method finally returns a version of the object with all nested paths converted to strings in dotted.string.notation. The above example as a flat object looks like this:

{
    'path': {to: {…}}
    'path.to': {string: 'string', integer: 42, float: 3.14, boolean: true, null: null}
    'path.to.boolean': true
    'path.to.float': 3.14
    'path.to.integer': 42
    'path.to.null': null
    'path.to.string': "string"
}

Chainability

Whenever a method would normally return an object, an elastic object with the same plugins will be returned instead. This means that you can chain methods, like this:

eObj.set('path.to.string', "foo").set('path.to.integer', 42).set('another.path.to.float', 3.14);
eObj.get('path.to.a.plain.object').filter(value => value > 3).values();

Adding properties

You aren't limited to Elastic Object's native functionality. It has a plugin system that allows you to pass an object of new methods as an argument to the constructor. /plugins/array.js and /plugins/native.js are loaded by default and you can check out these modules if you wish to add your own set of methods. Methods are added to the prototype chain, which may make them available project-wide depending on your implementation . Make sure that they implement the chainability paradigm as mentioned above.

Below is a short example of how this works. Keep in mind to use regular function syntax to ensure access to this.

const myPlugins = {
    methodA: function() {        
        console.log(this);
    }
    // more methods
}

const data = { bar: 42 };

// standard Elastic Object
const eObj1 = new ElasticObject(data);
eObj1.methodA(); // TypeError: eObj1.methodA is not a function

// Elastic Object with custom methods
const eObj2 = new ElasticObject(data, myPlugins);
eObj2.methodA(); // ElasticObject { bar: 42 }

// Load plugins after creation
const eObj3 = ElasticObject.create(data);
eObj3.methodA(); // TypeError: eObj1.methodA is not a function
eObj3.loadPlugins(myPlugins); // now it works
eObj3.methodA(); // ElasticObject { bar: 42 }



// the magic formula for chainability:
import isPlainObject from "whats-the-type/isPlainObject.js";

const foo = function() {
    const value = {a: 1}; // or whereever your value comes from
    return isPlainObject(value) ? this.create(value) : value;
}

Method overview

The following table is an overview of the available methods. The links in the first column point to this documentation, the second column to the original documentation which you might also find helpful.

Method External reference Notes
eObj.assign() Object.assign() Instance flavor: eObj.assign(...sources), this is the target
ElasticObject.assign() Object.assign() Static flavor: ElasticObject.assign(target, ...sources)
eObj.create() Object.create() Instance flavor: eObj.create(anyObj)
ElasticObject.create() Object.create() Static flavor: ElasticObject.create(anyObj)
eObj.clone() stucturedClone() Clones the whole object
eObj.cloneEntry() stucturedClone() Clones any property of the object
eObj.entries() Object.entries() Instance flavor: eObj.entries(), uses this
ElasticObject.entries() Object.entries() Static flavor: ElasticObject.entries(anyObj)
eObj.every() Array.every() Same signature and usage
eObj.filter() Array.filter() Returns an Elastic Object, chain .values() to get only the values
eObj.find() Array.find() Same signature and usage
eObj.findPath() Array.findIndex() Returns path in dotted.string.notation rather than an index
eObj.flatten() Flattened version of the object
eObj.forEach() Array.forEach() Same signature and usage
eObj.fromEntries() Object.fromEntries() Instance flavor: eObj.fromEntries(iteratable)
ElasticObject.fromEntries() Object.fromEntries() Static flavor: ElasticObject.fromEntries(iteratable)
eObj.get() getProperty() Same signature, but without the first argument object
eObj.has() hasProperty() Same signature, but without the first argument object
eObj.includes() Array.includes() Same signature and usage
eObj.keys() Object.keys() Instance flavor: eObj.keys(), uses this
ElasticObject.keys() Object.keys() Static flavor: ElasticObject.keys(anyObj)
eObj.length() Array.length In difference to Array.length, this is implemented as a function
eObj.map() Array.map() Same signature and usage
eObj.paths() Keys of the flattened object
eObj.reduce() Array.reduce() Same signature and usage
eObj.reduceRight() Array.reduceRight() Same signature and usage
eObj.set() setProperty() Same signature, but without the first argument object
eObj.some() Array.some() Same signature and usage
eObj.sort() Array.sort() Same signature and usage
eObj.toJson() JSON.stringify() With boolean pretty argument for pretty-print
eObj.unset() deleteProperty() Same signature, but without the first argument object
eObj.values() Object.values() Instance flavor: eObj.values(), uses this
ElasticObject.values() Object.values() Static flavor: ElasticObject.values(anyObj)

Resources