2.1.0 • Published 2 years ago

ramda-lens-groups v2.1.0

Weekly downloads
8
License
ISC
Repository
github
Last release
2 years ago

ramda-lens-groups

Full ramda-lens-group API documentation is here

ramda-lens-groups provides a set of utilities meant to help manage the complexity that can come along with creating lenses for objects with large sets of properties and nested objects. The ramda-lens-groups implementation relies on ramda lenses and utilities, and also makes heavy use of ramda-adjunct.

If you want to see why lenses are so handy, have a look here

A lens group is simply a collection of lenses that, as a whole, reference a set of properties associated with an object 'type'. Each lens in the group is focused on a particular property.

Conceptually, a lens group can be visualized like this:

const cat = {       // catLensGroup
  id:    1001,      //  <---- idLens
  name:  'fuzzball',//  <---- nameLens
  color: 'black',   //  <---- colorLens
  mood:  'aloof'    //  <---- moodLens
}

Creating A Lens Group

Lens groups are created by supplying a list of property names, optional defaults for those properties, and an optional path to the object in the case of nesting

import LG from 'ramda-lens-groups';

const catLg = LG.create (
  ['id', 'name',    'color',    'mood' ],   // prop names
  [-1,   'defName', 'defColor', 'defMood' ] // defaults
);

const catInMyLifeLg = LG.create (
  ['id', 'name',    'color',    'mood' ],    // prop names
  [-1,   'defName', 'defColor', 'defMood' ], // defaults
  [ 'pets', 'myCat']                         // path
);

Operating on individual properties of an object

const myCat = { name: 'sunshine', color: 'orange' };

const catLg = LG.create (
  ['id', 'name',    'color',    'mood' ],   // prop names
  [-1,   'defName', 'defColor', 'defMood' ] // defaults
);

LG.view(catLg, 'name', myCat); //=> 'sunshine'
LG.view(catLg, 'color', myCat); //=> 'orange'
LG.view(catLg, 'mood', myCat); //=> 'undefined'
LG.viewOr(catLg, 'mood', 'confused', myCat ); // 'confused'
LG.viewOrDef(catLg, 'mood', myCat); //=> 'defMood'

const moodyCat = LG.set(catLg, 'mood', 'grumpy', myCat);
LG.view(catLg, 'mood', moodyCat); //=> 'grumpy'
LG.viewOr(catLg, 'mood', 'confused', moodyCat ); //=> 'grumpy'
LG.viewOrDef(catLg, 'mood', moodyCat); //=> 'grumpy'
LG.view(catLg, 'mood', myCat); //=> undefined

Operating on objects nested within other objects

const myCat = { name: 'sunshine', color: 'orange' };
const myLife = { pets : { myCat }};

const catInMyLifeLg = LG.create (
  ['id', 'name',    'color',    'mood' ],    // prop names
  [-1,   'defName', 'defColor', 'defMood' ], // defaults
  [ 'pets', 'myCat']                         // path
);

LG.viewOrDef(catInMyLifeLg, 'name', myLife); //=> 'sunshine'
LG.viewOrDef(catInMyLifeLg, 'mood', myLife); //=> 'defMood'

const myMoodyLife = LG.set(catInMyLifeLg, 'mood', 'grumpy', myLife);
LG.viewOrDef(catInMyLifeLg, 'mood', myLife); //=> 'defMood'
LG.viewOrDef(catInMyLifeLg, 'mood', myMoodyLife); //=> 'grumpy'

Viewing the lens group target

A lens group can be used to extract the entire target from within a nested object.

const catInMyLifeLg = LG.create (
  ['id', 'name',    'color',    'mood' ],    // prop names
  [-1,   'defName', 'defColor', 'defMood' ], // defaults
  [ 'pets', 'myCat']                         // path
);
const myLife = { pets : { myCat }};
const myMoodyLife = LG.set(catInMyLifeLg, 'mood', 'grumpy', myLife);

LG.viewTarget(catInMyLifeLg, myLife); //=> { name: 'sunshine', color: 'orange' }
LG.viewTarget(catInMyLifeLg, myMoodyLife); //=> { name: 'sunshine', color: 'orange', mood: 'grumpy' }

Creating and cloning objects using lens groups

const myCat = { name: 'sunshine', color: 'orange' };
const myLife = { pets : { myCat }};

const catLg = LG.create (
  ['id', 'name',    'color',    'mood' ],   // prop names
  [-1,   'defName', 'defColor', 'defMood' ] // defaults
);

const catInMyLifeLg = LG.create (
  ['id', 'name',    'color',    'mood' ],    // prop names
  [-1,   'defName', 'defColor', 'defMood' ], // defaults
  [ 'pets', 'myCat']                         // path
);

LG.def(catLg); //=> { id: -1, name: 'defName', color: 'defColor', mood: 'defMood' }
LG.clone(catLg,myCat); //=> { name: 'sunshine', color: 'orange' }
LG.cloneWithDef(catLg,myCat); //=> { id: -1, name: 'sunshine', color: 'orange', mood: 'defMood' }
LG.clone(catInMyLifeLg,myLife); //=> { name: 'sunshine', color: 'orange' }

Specializing lens groups

A new lens group can be created as a specializaiton of an existing lens group

const myCat = { name: 'sunshine', color: 'orange' };

const catLg = LG.create (
  ['id', 'name',    'color',    'mood' ],   // prop names
  [-1,   'defName', 'defColor', 'defMood' ] // defaults
);


const catLgMinus = LG.remove(['id', 'mood'], catLg);
LG.def(catLgMinus); //=> { name: 'defName', color: 'defColor' }
const catLgPlus = LG.add(['weight'], [99], catLg);
LG.def(catLg); //=> { id: -1, name: 'defName', color: 'defColor', mood: 'defMood', weight: 99 }

const catShow = { houseCats: { myCat } };
const myCatInShowLg = LG.prependPath( ['houseCats', 'myCat'], catLg );
LG.viewTarget(myCatInShowLg, catShow);//=> { name: 'sunshine', color: 'orange' }

Creating your own custom functions

Lens group operators are curried, so that you can create your own custom functions

const myCat = { name: 'sunshine', color: 'orange' };
const catLg = LG.create (
  ['id', 'name',    'color',    'mood' ],   // prop names
  [-1,   'defName', 'defColor', 'defMood' ] // defaults
);

const viewCat = LG.view(catLg);
const viewCatOrDef = LG.viewOrDef(catLg);
const viewCatName = LG.view(catLg, 'name');

viewCat('color', myCat); //=> orange
viewCatOrDef('mood', myCat); //=> defMood
viewCatName(myCat); //=> sunshine

const cloneCat = LG.clone(catLg);
const cloneCatWithDef = LG.cloneWithDef(catLg);

cloneCat(myCat); //=> { name: 'sunshine', color: 'orange' }
cloneCatWithDef(myCat); //=> { id: -1, name: 'sunshine', color: 'orange', mood: 'defMood' }

Putting it all togehter, a more complex example

const myCat = { name: 'sunshine', color: 'orange' };

const catLg = LG.create (
  ['id', 'name',    'color',    'mood' ],   // prop names
  [-1,   'defName', 'defColor', 'defMood' ] // defaults
);

const secretCatLg = R.compose(
  LG.add(['secretName', 'secretPower', 'secretHandShake'], []),
  LG.remove(['id', 'mood'])
)(catLg);

const mySecretCat = R.compose(
  LG.set(secretCatLg, 'secretName', '009Lives'),
  LG.set(secretCatLg, 'secretPower', 'clawAttack'),
  LG.set(secretCatLg, 'secretHandShake', 'pawPound'),
  LG.cloneWithDef(secretCatLg)
)(myCat);
// =>
// { name: 'sunshine',
//    color: 'orange',
//    secretHandShake: 'pawPound',
//    secretPower: 'clawAttack',
//    secretName: '009Lives' }

const showEntryFormLg = LG.create(['whyParticipating', 'yourCat']);

const blankShowApplication = {
  whyParticipating: 'enter your reason for participating here',
  yourCat: 'put your primped cat here'
};

const showApplicationBeforePrimping = R.compose(
  LG.set(showEntryFormLg, 'whyParticipating', 'I like to show my cat off' ),
  LG.set(showEntryFormLg, 'yourCat', mySecretCat )
)(blankShowApplication);
// =>
// { whyParticipating: 'I like to show my cat off',
//     yourCat: {
//       name: 'sunshine',
//       color: 'orange',
//       secretHandShake: 'pawPound',
//       secretPower: 'clawAttack',
//       secretName: '009Lives' }}

const showCatLg = R.compose(
  LG.appendPath(['yourCat']),
  LG.remove(['secretName', 'secretPower', 'secretHandShake']),
  LG.add(['breed'], ['fancy breed']),
  LG.add(['mood'], ['sociable'])
)(secretCatLg);

const showApplicationAfterPrimping = LG.setTarget(
  showCatLg,
  LG.cloneWithDef(showCatLg, showApplicationBeforePrimping),
  showApplicationBeforePrimping);
//=>
// { whyParticipating: 'I like to show my cat off',
//   yourCat: {
//     name: 'sunshine',
//     color: 'orange',
//     mood: 'sociable',
//     breed: 'fancy breed' } }

const driveToShow = ()=> 'hwy 66, first left after the ocean';
const presentAtShow = LG.viewTarget(showCatLg);

driveToShow();
presentAtShow(showApplicationAfterPrimping);
//=>
// { name: 'sunshine',
//   color: 'orange',
//   mood: 'sociable',
//   breed: 'fancy breed' }

Full ramda-lens-group API documentation is here

Why Lenses

Lenses greatly simplify 2 tasks

  • Handling fault tolerant access to values on nested objects
  • Honoring immutability when setting values within nested object

Fault tolerant property access

Lets have a look at this old friend, a server response which may or may not be complete

const rsp1 = { data : {items: ['i1', 'i2']}}; // expected case
const rsp2 = { data : {items: undefined }};   // oops
const rsp3 = { data : undefined};             // "
const rsp4 = undefined;                       // "

console.log(rsp1.data.items.length); //=> 2
console.log(rsp2.data.items.length); //=> cannot read property 'length' of undefined (doh!)
console.log((rsp2.data.items||[]).length); //=> 0 (klunky)
console.log((rsp3.data.items||[]).length); //=> Cannot read property 'items' of undefined (doh!)
console.log(((rsp3.data||{}).items||[]).length); //=> 0 (super klunky)
console.log(((rsp4.data||{}).items||[]).length); //=> Cannot read property 'data' of undefined
console.log((((rsp4||{}).data||{}).items||[]).length); //=> 0 (works for all cases, but klunky^3)

Using ramda lenses and a ramda-adjunct helper, the klunkiness vanishes. RA.viewOr() which is based on R.view() is smart enough to handle a breakage anywhere in the path to the value that you are after.

const itemsLens = R.lensPath(['data','items']);
var viewItems = RA.viewOr([], itemsLens); // partially applied fxn, works in all cases

console.log((viewItems(rsp1)).length); //=> 2
console.log((viewItems(rsp2)).length); //=> 0
console.log((viewItems(rsp3)).length); //=> 0
console.log((viewItems(rsp4)).length); //=> 0

Honoring immutability when setting object properties

Lets look at the very common case of immutably updating an existing state object

const state = {
  animals : { dogs: 'fido', cat: 'garfield' },
  hobby : 'pets'
};

// The approach below is allot of work and error prone
// As state gets broader and deeper, it can become a bit of a nightmare
const newState = {
  ...state,
  animals: {
    dogs: state.animals.dogs,
    cats: 'tiger'
  },
};

console.log(state); //=> { animals: { dogs: 'fido', cat: 'garfield' }, hobby: 'pets'
console.log(newState); //=> { animals: { dogs: 'fido', cats: 'tiger' }, hobby: 'pets' }

The Ramda lens operation used to set values within nested objects honors immutability. This is very powerful when managing complex states immutably

const state = {
  animals : { dogs: 'fido', cat: 'garfield' },
  hobby : 'pets'
};

const catsLens = R.lensPath(['animals', 'cat'] );

// Very straight forward (compare to the 7 liner in the previous example).
// Does not mutate state!
const newState2 = R.set(catsLens, 'tiger' , state );

console.log(state); //=> { animals: { dogs: 'fido', cat: 'garfield' }, hobby: 'pets' }
console.log(newState2); //=> { animals: { dogs: 'fido', cat: 'tiger' }, hobby: 'pets' }

Full ramda-lens-group API documentation is here

2.1.0

2 years ago

2.0.1

4 years ago

2.0.0

4 years ago

1.2.3

6 years ago

1.2.2

6 years ago

1.2.1

7 years ago

1.2.0

7 years ago

1.1.0

7 years ago

1.0.5

7 years ago

1.0.4

7 years ago

1.0.3

7 years ago

1.0.2

7 years ago

1.0.1

7 years ago

1.0.0

7 years ago