1.1.0 • Published 7 years ago

point-less v1.1.0

Weekly downloads
-
License
ISC
Repository
-
Last release
7 years ago

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 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; // 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 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