point-less v1.1.0
point-less
installation
yarn add point-lessor
npm i point-lessconcepts
lens methods
Lenses are a very deep, abstract concept, but in practice it can still be useful
to use only the two most basic lens operations, view and over, on the most
basic type of lens, which essentially is just a path.
view(path, object) reads the value of object at the path path.
over(path, updater, object) updates the value of object at the path path,
to be whatever the function updater returns. updater receives the current
value as an argument. So for example,
const path = ['info', 'count'];
const updater = x => x + 1;
const object = {
info: {
name: 'fran',
count: 7
}
};
over(path, updater, object); // { info: { name: 'fran', count: 8 } }The third basic lens method set is a special case of over, where the
updater is a constant function. set(path, value, object) will set the
value of object at the path path to be value (which is equivalent to
over(path, () => value, object)).
path maps
A path is an array of values corresponding to chained property lookups in an
object. For example, the path ['name', 'first'] in an object person would
correspond to person.name.first.
A pathMap is an object where every value is either a path or a pathMap.
The keys of a pathMap are called pathAliases.
For example:
const personMap = {
firstName: ['name', 'first'],
lastName: ['name', 'last'],
age: ['age']
};
const familyMap = {
mom: personMap,
dad: personMap,
name: ['name']
};Here, familyMap would (partially) describe the following object:
const family = {
mom: {
name: {
first: 'Shirley',
last: 'Schmidt'
},
age: 70
},
dad: {
name: {
first: 'Denny',
last: 'Crane'
},
age: 86
},
name: 'Crane'
}module api
Given a pathMap and lens methods { view, set, over }, this library aims
to automate some of the boilerplate inherent in reading and updating state.
pointless(pathMap, lensInterface = vanilla)
Takes a pathMap as input and returns a stateInterface instance. It accepts
an optional second argument lensInterface, which describes how data is read
and updated. The default lensInterface, vanilla, works on plain JS objects.
enhanceLensInterface(lensPath, view, over)
Generates a lensInterface instance based on the three atomic functions
provided. These functions should act in the same way as their ramda namesakes
(to generate the vanilla interface, the ramda functions themselves are used).
vanilla
A lensInterface instance used for working with regular JS objects.
immutable
A lensInterface instance used for working with immutable objects (caveat:
the way this is implemented is really lazy, but it's enough to cover all
functionality provided by the stateInterface API, as of now).
stateInterface api
view, set, over
Each of the three basic lens operations have properties corresponding to the
properties of the given pathMap. For example:
const { pointless } = require('point-less');
const pathMap = { firstName: ['name', 'first'] };
const { view, over } = pointless(pathMap);
const brahms = { name: { first: 'johannes'} };
view.firstName(brahms); // 'johannes'
const capitalize = name => name[0].toUpper() + name.slice(1);
over.firstName(capitalize, brahms); // { name: { first: 'Johannes' } }view[pathAlias](state)
Reads the value of state at the path associated to pathAlias.
set[pathAlias](value, state)
Sets the value of state to value, at the path associated to pathAlias.
over[pathAlias](updater, state, ...extraArgs)
Sets the value of state at the path associated to pathAlias. The new
value is computed as updater(state, ...extraArgs).
at(...subpathArgs)
at returns a relativeStateInterface at a path computed from subpathArgs.
That is, it uses the same pathMap given to the original state interface,
but all paths are implicitly prepended with the extra path given to at. As a
result, you can read and update an object even if the pathMap only applies to
a subobject. For example:
const pathMap = {
lastName: ['name', 'last']
};
const data = {
users: [
{ name: { first: 'debra', last: 'messing' } },
{ name: { first: 'debra', last: 'missing' } }
]
};
const substate = pointless(pathMap).at(['users', 1]);
substate.view.lastName(data); // 'missing'
// updates the last name in the context of the full object; `newData` is still
// the same shape as `data`.
const newData = substate.set.lastName('amassing', data);
newData.users[1]; // { name: { first: 'debra', last: 'amassing' } }A relativeStateInterface is also a stateInterface, so the API of the latter
also applies to the former. In addition, a relativeStateInterface also has an
all property. This gives a full { view, set, over } interface on the
entire object at the subpath. Continuing the above:
const newUser = { name: { first: 'deer', last: 'hunter' } };
const newData = substate.all.set(newUser, data);
newData.users[1] === newUser; // trueThe subpath is computed from ...subpathArgs by fully flattening the array
subpathArgs. That is, at(['a', 'b', 'c']) is equivalent to
at('a', 'b', 'c') and at([[[['a']]], ['b']], 'c').
In addition, any function in the flattened subpath array can be used to
dynamically select a path after the state has been received. Any function
found in subpath will receive as arguments both state and any other
arguments after state. Continuing the above:
const substate = pointless(pathMap).at('users', (_, action) => action.index);
const updateFirst = substate.firstName.over((_, action) => action.firstName);
const newData = updateFirst(data, { index: 1, firstName: 'barbara' });
newData.users[1]; // { name: { first: 'barbara', last: 'massing' } };overWith(selectors, updater, state)
overWith is a shortcut for a special case of an over call, to facilitate
using multiple pieces of the current state to update multiple pieces.
selectors is an array of functions which take the state as input.
Whatever they return is passed into the arguments of updater,
another user-provided function. Note that the selectors don't need to be
simple property reads from a stateInterface; any function that takes the
state as input, such as a memoized selector, would work.
The return value of updater must be an object updates, whose keys are
pathAliases in pathMap. Each pathAlias will have its corresponding value
in state set to updates[pathAlias]. As a somewhat contrived example:
const pathMap = {
birthDay: ['dob', 'day'],
birthMonth: ['dob', 'month'],
birthYear: ['dob', 'year'],
birthStr: ['birthStr']
};
const { view, overWith } = pointless(pathMap);
const computeBirthStr = overWith(
[view.birthDay, view.birthMonth, view.birthYear],
(day, month, year) => ({ birthStr: [month, day, year].join('/') })
);
const data = { dob: { day: 15, month: 5, year: 1992 } };
computeBirthStr(data); // { dob: { ... }, birthStr: '5/15/1992' }other functionality
functions in paths
A function can be used as an element of a path array, when the path should depend on the current state or otherwise won't be known until a lens operation is called. The motivating case for this was to pick out an index in an array, for example:
const actionUser = ['users', (state, action) => action.index];
const pathMap = {
actionUser,
actionUsername: [...actionUser, 'name']
};
const data = {
users: [{ name: 'momoney' }, { name: 'moproblems' }]
};
const state = pointless(pathMap);
state.view.actionUser(data, { index: 1 }); // { name: 'moproblems' };
const newData = state.over.actionUsername(
(state, action) => action.newName,
data,
{ index: 1, newName: 'moped' }
);
newData; // { users: [{ name: 'momoney' }, { name: 'moped' }] }nested path maps
As stated above, a pathMap can have another pathMap as one of its values.
In that case, the pathAlias follows the same chain of properties as in the
pathMap:
const userGroupPathMap = {
count: ['count'],
};
const nestedPathMap = {
online: userGroupPathMap,
offline: userGroupPathMap
};
const data = {
online: { count: 7 },
offline: { count: 100 }
};
const state = pointless(nestedPathMap);
state.view.online.size(data); // 7
state.view.offline.size(data); // 100