3

Assuming the follow object how is it possible to use Ramda to perform a nested update in a criteria given an application, criteria ID and data?

const application = {
  id: 'a1',
  features: [
    {
      id: 'f1',
      criterias: [
        { id: 'c1' }
      ]
    },
    {
      id: 'f2',
      criterias: [
        { id: 'c2' },
        { id: 'c3' }
      ]
    }
  ]
}

The function would look something like this:

const updateCriteria = (application, criteriaId, data) => // magic...

updateCriteria(application, 'c2', { name: 'foo' })

// output: {
//  id: 'a1',
//  features: [
//    {
//      id: 'f1',
//      criterias: [
//        { id: 'c1' }
//      ]
//    },
//    {
//      id: 'f2',
//      criterias: [
//        { id: 'c2', name: 'foo' },
//        { id: 'c3' }
//      ]
//    }
//  ]
// }

1 Answers1

6

Lenses are probably your best bet for this. Ramda has a generic lens function, and specific ones for an object property (lensProp), for an array index(lensIndex), and for a deeper path(lensPath), but it does not include one to find a matching value in an array by id. It's not hard to make our own, though.

A lens is made by passing two functions to lens: a getter which takes the object and returns the corresponding value, and a setter which takes the new value and the object and returns an updated version of the object.

Here we write lensMatch which find or sets the value in the array where a given property name matches the supplied value. And lensId simply passes 'id' to lensMatch to get back a function which will take an id value and return a lens.

Using any lens, we have the view, set, and over functions which, respectively, get, set, and update the value.

We could use idLens like this:

const data = [{id: 'a'}, {id: 'b'}, {id: 'c'}]

view (idLens ('b'), data) 
  //=> {id: 'b'}
set  (idLens ('b'), 'foo', data) 
  //=> [ {id: 'a'}, 'foo', {id: 'c'} ]
over (idLens ('b'), merge ({name: 'foo'}), data)
  //=> [ {id: 'a'}, {id: 'b', name: 'foo}, {id: 'c'} ]

So for your problem, we could write something like this:

const lensMatch = (propName) => (key) => lens
  ( find ( propEq (propName, key) )
    , (val, arr, idx = findIndex (propEq (propName, key), arr)) =>
         update (idx > -1 ? idx : length (arr), val, arr)
    )

const lensId = lensMatch ('id')

const updateCriteria = (featureId, criteriaId, data, application) => over 
  ( compose 
      ( lensProp ('features')
      , lensId (featureId) 
      , lensProp ('criterias') 
      , lensId (criteriaId)
      )
    , merge (data)
    , application
    )

const application = {id: 'a1', features: [{id: 'f1', criterias: [{ id: 'c1' }]}, {id: 'f2', criterias: [{ id: 'c2' }, { id: 'c3' }]}]}

const newApp = updateCriteria ('f2', 'c2', {name: 'foo'}, application)

console.log(newApp)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>
const {lens, find, propEq, findIndex, update, length, view, set, over, compose, lensProp, merge} = R
</script>

But this presupposes that you know the featureId. If you need to find both the featureId and the nested object with your internal id, you could write a more complex lens for that, but it will be much more heavyweight.


A minor note: 'criteria' is already plural, so 'criterias' is odd. The singular is 'criterion'.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 2
    great primer for someone being introduced to topic of lenses – Mulan Feb 21 '19 at 17:43
  • Scott, I did learned a lot of things in your answer. Thank you for that! I'll have to write a lens to find the feature ID as well to this complexity in only one layer. I'm aware of the trade-offs of scanning thru the application structure over and over again but right now I'm not worried about them now. – William Calderipe Feb 23 '19 at 00:12