point-less
v1.1.0
Published
pointless state interface
Downloads
3
Readme
point-less
installation
yarn add point-less
or
npm i point-less
concepts
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 pathAlias
es.
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; // true
The 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
pathAlias
es 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 path
s
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