Reputation: 365
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
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
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