omygoodness
omygoodness

Reputation: 365

Ramda - how to add new properties to nested object

I am trying to add new properties width and height to nested objects.

My data structure looks like this:

const graph = {
  id: 'root',
  children: [
    {
      id: 'n1'
    },
    {
      id: 'n2'
    }
  ]
};

I am trying to add unique width and height properties to each child based on id

I tried R.lensPath. Here you can check it in ramda editor:

const widthLens = R.curry((id, data) => R.lensPath([
  'children', 
  R.findIndex(R.whereEq({ id }),
  R.propOr([], 'children', data)),
  'width',
]));

const setWidth = widthLens('n1', graph);
R.set(setWidth, '100', graph);

And this is working almost as it should but it is adding only width plus I need to iterate over all children and return the same object with new properties. It also looks overcomplicated so any suggestions are more than welcome. Thank you.

Upvotes: 3

Views: 1099

Answers (2)

Scott Sauyet
Scott Sauyet

Reputation: 50807

There are several different ways of approaching this. But one possibility is to use custom lens types. (This is quite different from Ori Drori's excellent answer, which simply uses Ramda's lensPath.)

Ramda (disclaimer: I'm one of the authors) only supplies only a few specific types of lenses -- one for simple properties, another for array indices, and a third for more complex object paths. But it allows you to build ones that you might need. And lenses are not designed only for simple object/array properties. Think of them instead as a framing of some set of your data, something you can focus on.

So we can write a lens which focuses on the array element with a specific id. There are decisions to make about how we handle missing ids. I'll choose here -- if the id is not found -- to return undefined for a get and to append to the end on a set, but there are reasonable alternatives one might explore.

In terms of implementation, there is nothing special about id, so I will do this based on a specific named property and specialize it to id in a separate function. We could write 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')

It would work like this:

const lens42 = lensId (42)

const a = [{id: 17, x: 'a'}, {id: 42, x: 'b'}, {id: 99, x: 'c'}, {id: 57, x: 'd'}]

view (lens42, a) //=> {id: 42, x: 'b'}
set (lens42, {id: 42, x: 'z', foo: 'bar'}, a)
//=> [{id: 17, x: 'a'}, {id: 42, x: 'z', foo: 'bar'}, {id: 99, x: 'c'}, {id: 57, x: 'd'}]
over (lens42, assoc ('foo', 'qux'), a)
//=> [{id: 17, x: 'a'}, {id: 42, x: 'b', foo: 'qux'}, {id: 99, x: 'c'}, {id: 57, x: 'd'}]

But then we need to deal with our width and height properties. One very useful way to do this is to focus on an object with given particular properties, so that we get something like {width: 100, height: 200}, and we pass an object like this into set. It turns out to be quite elegant to write:

const lensProps = (props) => lens (pick (props), mergeLeft)

And we would use it like this:

const bdLens = lensProps (['b', 'd'])

const o = ({a: 1, b: 2, c: 3, d: 4, e: 5})
view (bdLens, o) //=> {b: 2, d: 4}
set (bdLens, {b: 42, d: 99}, o) //=> {a: 1, b: 42, c: 3, d: 99, e: 5}
over (bdLens, map (n => 10 * n), o) //=> {a: 1, b: 20, c: 3, d: 40, e : 5}

Combining these, we can develop a function to use like this: setDimensions ('n1', {width: 100, height: 200}, graph) We first write a lens to handle the id and our dimension:

const lensDimensions = (id) => compose (
  lensProp ('children'), 
  lensId (id), 
  lensProps (['width', 'height'])
)

And then we call the setter of this lens via

const setDimensions = (id, dimensions, o) => 
  set (lensDimensions (id), dimensions, o)

We can put this all together as

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 lensProps = (props) => lens (pick (props), mergeLeft)

const lensId = lensMatch ('id')

const lensDimensions = (id) => compose (
  lensProp ('children'), 
  lensId (id), 
  lensProps (['width', 'height'])
)

const setDimensions = (id, dimensions, o) => set (lensDimensions (id), dimensions, o)

const graph = {id: 'root', children: [{id: 'n1'}, {id: 'n2'}]}

console .log (setDimensions ('n1', {width: 100, height: 200}, graph))
//=> {id: "root", children: [{ id: "n1", height: 200, width: 100}, {id: "n2"}]}
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
<script> const {find, propEq, findIndex, update, length, lens, pick, mergeLeft, compose, lensProp, set} = R </script>

This clearly involves more lines of code than does the answer from Ori Drori. But it creates the useful, reusable lens creators, lensMatch, lensId, and lensProps.

Note: This as is will fail if we try to work with unknown ids. I have a fix for it, but I don't have the time right now to dig into why it fails, probably something to do with the slightly unintuitive way lenses compose. If I find time soon, I'll dig back into it. But for the moment, we can simply change lensProps to

const lensProps = (props) => lens (compose (pick (props), defaultTo ({})), mergeLeft)

And then an unknown id will append to the end:

console .log (setDimensions ('n3', {width: 100, height: 200}, graph))
//=> {id: "root", children: [{id: "n1"}, {id: "n2"}, {id: "n3", width : 100, height : 200}]}

Upvotes: 4

Ori Drori
Ori Drori

Reputation: 193027

You can use R.over with R.mergeLeft to add the properties to the object at the index:

const { curry, lensPath, findIndex, whereEq, propOr, over, mergeLeft } = R;

const graph = {"id":"root","children":[{"id":"n1"},{"id":"n2"}]};

const widthLens = curry((id, data) => lensPath([
  'children', 
  findIndex(whereEq({ id }), propOr([], 'children', data)),
]));

const setValues = widthLens('n1', graph);
const result = over(setValues, mergeLeft({ width: 100, height: 200 }), graph);

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js" integrity="sha512-rZHvUXcc1zWKsxm7rJ8lVQuIr1oOmm7cShlvpV0gWf0RvbcJN6x96al/Rp2L2BI4a4ZkT2/YfVe/8YvB2UHzQw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

Upvotes: 3

Related Questions