Add data dynamically to an object recursively - javascript

Given the following code:
const data = {
name: 'product',
children: [
{ name: 'title' },
{ name: 'code' },
{ name: 'images', children: [{ name: 'image1' }, { name: 'image2' }] },
],
}
let result = {}
function resolveNodes(nodes, parentNode){
if(nodes.children){
parentNode = nodes.name;
result[parentNode] = {};
nodes.children.forEach(node => resolveNodes(node, parentNode));
}else{
result[parentNode][nodes.name] = nodes.name;
}
}
resolveNodes(data);
console.log(result);
Current output
{
product: {
title:"title",
code:"code"
},
images: {
image1:"image1",
image2:"image2"
}
}
Desired output:
{
product: {
title:"title",
code:"code",
images: {
image1:"image1",
image2:"image2"
}
}
}
I'm not sure how to recursively add a nested object within the parent (product) object. I've tried many things like passing the parent and the child to the function and from there try to work out the structure of the new object however it get very messy and I know is not the ideal way to do it.
This function will need to handle at least one extra layer of children but I've just added 2 to simplify the question.
What is the most ideal way to implement this?

You could take a recursive approach and have a look to children and get a combined object, otherwise just the name as value.
Instead of giving a simplified solution, let's have a closer look to the code.
resolveNodes = ({ name, children }) => ({ [name]: children
? Object.assign(...children.map(resolveNodes))
: name
})
The first part,
resolveNodes = ({ name, children }) =>
^^^^^^^^^^^^^^^^^^
the arguments of the function, contains a destructuring assignment, where an object is expected and from this object, the named properties are taken as own variables.
The following returned expression
resolveNodes = ({ name, children }) => ({
})
contains an object with a
vvvvvvv
resolveNodes = ({ name, children }) => ({ [name]:
})
computed property name, which takes the value of the variable as property name.
The following conditional (ternary) operator ?:
children
? Object.assign(...children.map(resolveNodes))
: name
checks children.
If this value is truthy, like an array, it evaluates the part after ? and if the value is falsy, the opposite of truthy, then it takes the expression after :.
In short, if no children, then take name as property value.
The part for truthy, if children exists,
Object.assign(...children.map(resolveNodes))
returns a single object with all children properties.
Object.assign(...children.map(resolveNodes))
^^^^^^^^^^^^^ create a single object
^^^ spread syntax
^^^^^^^^^^^ return all element by using
^^^^^^^^^^^^ the actual function again
This part is easier to understand if order of execution is followed.
children.map(resolveNodes)
the inner part with a mapping of children by calling resolveNodes returns an array of objects with a single property. It looks like this
[
{ title: 'title' },
{ code: 'code' },
{ images: // this looks different, because nested
{ // children are called first
image1: 'image1',
image2: 'image2'
}
}
]
and contains objects with a single property.
To get an single object with all properties of the array, Object.assign takes objects and combines all objects to one and replaces same properties in the target object with the latest following same named property. This is not the problem here, because all properties are different. Actually the problem is to take the array as parameters for calling the function.
This problem has two solutions, one is the old call of a function with Function#apply, like
Object.assign.apply(null, children.map(resolveNodes))
The other one and used
Object.assign(...children.map(resolveNodes))
is spread syntax ..., which takes an iterable and adds the items as parameters into the function call.
const
resolveNodes = ({ name, children }) => ({ [name]: children
? Object.assign(...children.map(resolveNodes))
: name
}),
data = { name: 'product', children: [{ name: 'title' }, { name: 'code' }, { name: 'images', children: [{ name: 'image1' }, { name: 'image2' }] }] },
result = resolveNodes(data);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Related

Javascript destructuring and function as an object

Could someone possibly help me understand this function and destructuring a little better?
export default ({ names }) => {
const {
people: {
children = []
} = {}
} = names;
children.find((child) => child.email);
};
I understand that names is being destructured to extract data stored in objects in order to find the first child in the children's array that has an email address. But what I don't understand is why is this function declared like this ({ names })? Is it because names is an object? Sorry if this is a silly question but I am struggling to understand this.
Lets break it down:
Your function takes in 1 parameter which is an object...
...that object must have a property names.
Then the function destructures the object as follows (const { people: { children = [] } = {} } = names;):
First, you destructurize a property called people from the names argument
If people doesn't exist, it takes the default value of {}.
And finally, it grabs the property children (which are an array of
objects) from its parent people.
Next, the function logic with .find()
All it does is searching for a child from children from people from names from the argument object that has a property email...
...and returns it. Unfortunately that part is missing in your function code.
So in your snippet, the function actually does absolutely nothing, except to be unnecessarily complicated :P
To wrap things up. This is what your argument to the function could look like:
const argument = {
names: {
people: {
children: [{ email: "myemail#mail.com", name: "thechildtobefound" }],
}
}
};
And here's a working sample to try:
const func = ({ names }) => {
const { people: { children = [] } = {} } = names;
return children.find((child) => child.email);
};
const argument = {
names: {
people: {
children: [{ email: "myemail#mail.com", name: "thechildtobefound" }],
},
},
};
console.log(func(argument));

How to use TypeScript correctly for map function?

I have this DB, and I'm trying correctly map the types when using Map function. First time doing it so trying to learn but I'm stuck.
Db
const db = {
data: [
{
id: 1,
contentType: ['video'],
title: '1 to 1 Coaching: Sprinting Technique',
author: {
name: 'James',
image: {
alt: 'smiling.',
},
},
image: {
alt: 'Two footballers jostling for the ball with a coach watching in the background.',
},
},
],
}
For now I'm just using sample type of id and title but even here I'm stuck on how to correctly map it so TS does not complain :(
App.tsx
type Data = {
id: number
title: string
}
interface Customer {
id: number
title: string
}
export default function Box(): JSX.Element {
const [data, setData] = useState([db])
return (
<>
{data.map(
({
data: {
id,
contentType,
title,
author: {
name,
img: { src, alt },
},
image: { src, alt },
},
}): JSX.Element => {
console.log(id)
return <div css={mainBox}></div>
},
)}
</>
)
}
This issue has little to do with TS itself since the error is at object destructuring in parameters list of your map callback.
When you apply object destructuring to the argument of the function, JS/TS assigns properties of the object to distinct variables with the same name (unless they are objects and they are destructured in a similar way).
Let's have a look at author property destructuring:
...
author: {
name,
img: { src, alt },
},
...
This snippet will be treated as following:
author.name property will be assigned to distinct name variable. However,
author.img won't be assigned to distinct img variable since it is an object and it is desctructured.
It's properties would be assigned to distinct variables src and alt respectively.
After that we have another property desturctured:
image: { src, alt },
Again, image won't be assigned to distinct variable since it's object being destructured. But it's properties should.
It's properties are src and alt. And they should be set to variables with these names but we already have those names come from destructuring author.img.
So there is name collision in variables definition and that's why TS compiler raised an error.
You may fix this by assigning properties to new variable names, for example, the following way:
({
data: {
id,
contentType,
title,
author: {
name,
img: { src: authorImgSrc, alt: authorImgAlt },
},
image: { src, alt },
},
}): JSX.Element => {
// src refers to image.src, authorImgSrc refers to author.name.src
// alt refers to image.alt, authorImgAlt refers to author.name.alt
}
Here we explicitly assigned author.img properties to variables with different names so there is no name collision anymore.
The destructuring assignment technique might be really useful, there are more examples of it on MDN
After that you might see that you might not even need map in the first place, because you used array destructuring here:
const [data] = useState([db]);
And after that you may have access to it's internals directly:
const {img} = data.data.author;
So you may want to either remove .map (and render db entry directly) or not use array destructuring in data variable initialization:
const data = useState([db]);

Why isn't the prop reactive in the dynamic component?

I have a dynamic component being injected in the slot of another one and I pass the props object to it. But when I update the data (an array) which has been assosiated with the prop (dataT: this.tableData), that prop isn't being updated inside the component.
It seems like I have a deal with two different objects but the array was passed by the reference, wasn't it?
This is the main component
<template>
<Button #click="addWindows"></Button>
<Window v-for="window in windows" :key="window.id">
<component :is="window.name" v-bind="window.props" #onDeleteRow="handleDeleteRow"></component>
</Window>
</template>
<script>
export default{
data(){
return{
windows:[],
tableData:[
{
id: '0',
name: 'dog'
},
{
id: '1',
name: 'cow'
},
{
id: '2',
name: 'cat'
}
]
}
},
methods:{
addWindows(){
this.windows = [
{
id: 0,
name: 'Component1',
props: {
dataT: this.tableData
}
},
{
id: 1,
name: 'Component2',
props: {}
}];
},
handleDeleteRow(id){
this.tableData = this.tableData.filter(r => r.id != id);
}
}
}
</script>
I expect updating dataT prop in Component1 when I modify this.tableData in the main component.
Original answer based on an earlier version of the question
If you make windows a computed property it can depend on tableData:
export default {
data() {
return {
tableData: [
{
id: '0',
name: 'dog'
},
{
id: '1',
name: 'cow'
},
{
id: '2',
name: 'cat'
}
]
}
},
computed: {
windows () {
return [
{
id: 0,
name: 'Component1',
props: {
dataT: this.tableData
}
}, {
id: 1,
name: 'Component2',
props: {}
}
]
}
}
}
If you can't make all of it a computed property, e.g. because you need to be able to modify it, then keep it as data and just use the computed property to create the array needed in your template. In that case the computed property would just be merging together different parts of the data into the correct form.
In your original code, the line dataT: this.tableData won't work because this.tableData doesn't exist yet, it'll just be undefined. There's no lazy evaluation here, it needs to resolve to the correct object at the point it hits that line.
Even if it was able to get access to the correct object it wouldn't help because in handleDeleteRow you're reassigning tableData to point to a different object. Passing 'by reference' has nothing to do with the name you use to identify the object, it refers to a reference in memory.
Incidentally, v-on also supports an object syntax, just like v-bind, so you could make onDeleteRow optional in a similar fashion.
Update based on the edited question
When you write this in addWindows:
props: {
dataT: this.tableData
}
This will assign the current value of this.tableData to dataT. That current value will be an array and as arrays are reference types any modifications made to that array will apply no matter what identifier is used to reference it.
However, this line...
this.tableData = this.tableData.filter(r => r.id != id);
... does not modify that array. Instead it assigns a totally new array to this.tableData. This will have no effect on the array referenced by dataT, which is unchanged.
There are several ways you could approach solving this, including using a computed property. However, a property getter might provide a convenient sleight-of-hand:
addWindows () {
const vm = this;
this.windows = [
{
id: 0,
name: 'Component1',
props: {
get dataT () {
return vm.tableData
}
}
},
{
id: 1,
name: 'Component2',
props: {}
}
];
}
This will always evaluate to the current value of tableData. Vue's reactivity should be fine with this extra indirection, it just sees it as equivalent to accessing tableData directly.
TL;DR
The issue is with your binding. Use the following:
<component
:is="window.name"
:dataT="window.props.dataT"
#onDeleteRow="handleDeleteRow">
</component>
Explanation
the v-bind attribute specifies what prop is bound to what value (or reference). In your case, you didn't specify what values you're binding to what props, thus the component props weren't bound as expected.

Delete a json inside another in React

Imagine that I have this kind of JSON object on my react state:
this.state={
parent:{
childs:[
child1:{
},
child2:{
},
child3:null,
(...)
]
}
}
to delete the child1 I did the following method:
deleteChild1 = (index,test) => {
const childs= [...this.state.parent.childs];
childs[index] = {
...childs[index],
child1: null
}
this.setState(prevState => ({
...prevState,
parent: {
...prevState.parent,
childs: [
...childs,
]
}
}))
}
This works with no problem, but imagine that I have 100 childs, I have to do 100 methods like this but instead putting the child1 to null I have to put the child100, child99, you get the idea.
My question is that is another way to put the variable null.
Thanks!
Currently your data structure isn't valid so its hard to write up a solution. If I try to create that exact state object it raises an exception Uncaught SyntaxError: Unexpected token : This is because you have an array written like an object. So first thing to do is adjust your state model to be valid syntax
this.state = {
parent: {
childs: {
child1: {
},
child2: {
},
child3: null,
...
}
}
}
Now, what you are describing / what you want to do is a dynamic key reference.
You can do this like so
const childTarget = 'child1'
childs = {
...childs,
[childTarget]: null
}
so to abstract that concept a little more. make it a function parameter
deleteChild = (childTarget) => {
then when you want to remove any particular child you can let them pass their value to this function
<Child onRemove={this.deleteChild} name={name} />
// name here would be like 'child1'
// assuming you are looping over state and rendering a child component for each item
and when the child calls this function
this.props.onRemove(this.props.name)
The answer is very simple, this would be my approach.
Create a function which updates your state with the expect result (removing that property).
If you wish to assign null that you can replace .filter() with a .map() solution. Typically if you are removing a piece of data it does not make sense to null it, but to remove it.
FYI your property childs is an array you have an object within, so you need a list of objects to fix this.
E.g.
[
{
name: "child1"
},
{
name: "child2"
},
{
name: "child3"
}
]
removeChild = (child) => {
const newChildList = this.state.parent.childs.filter(({name}) => name !== child);
this.setState(previousState => ({
...previousState,
parent: {
...previousState.parent,
childs: newChildList
}
}));
}
The key part here is that the data is being updated and overriding the original array. Because you have nested data structure we don’t want to delete any pre-existing data (hence the spreading).
Call the function however you want and if your childs array has an object with the property called name that matches the child function argument, it will be not be present on the next re-render.
Hope this helps.

JavaScript object array filtering with embedded array

I have an object that looks like the following:
let responseData = [
{
"name": "name",
"other": "value",
"anotherField": "blue",
"appRoles": [
{
"code": "roleOne",
"shouldDisplay": true
},
{
"code": "roleTwo",
"shouldDisplay": false
}
]
}
I need to maintain the original structure all while keeping existing properties. I only want to remove/filter out any "appRoles" where "shouldDisplay" is false.
The following works, using a forEach and a filter operation to create a new object array, but is it possible to condense this even more?
let filteredApps;
responseData.forEach((team) => {
let indyTeam = team;
indyTeam.appRoles = team.appRoles.filter((role) => role.shouldDisplay === true);
filteredApps.push(indyTeam);
});
When I use the map operation, I only get an array of the filtered appRoles - missing extra properties on each object such as "name":
let enabledAppRolesOnly =
responseData.map((team) =>
team.appRoles.filter((role) => role.shouldDisplay === true));
array.map function calls a callback for each element of your array, and then push the return value of it to a new array.
from MDN doc:
map calls a provided callback function once for each element in an array, in order, and constructs a new array from the results. callback is invoked only for indexes of the array which have assigned values, including undefined. It is not called for missing elements of the array (that is, indexes that have never been set, which have been deleted or which have never been assigned a value).
So in your case, since you return team.appRoles.filter((role) => role.displayByDefault === true) which is your team array, you only get this.
What you could do would be this (in order to fully clone the object):
let responseData = [{
"name": "name",
"appRoles": [
{
"code": "roleOne",
"shouldDisplay": true
},
{
"code": "roleTwo",
"shouldDisplay": false
}
]
}]
let enabledAppRolesOnly = responseData.map(team => {
const appRoles = team.appRoles.filter(role => role.shouldDisplay === true)
return Object.assign({}, team, { appRoles })
});
console.log(enabledAppRolesOnly)
This will achieve your objective non-destructively. It will build a new array for you.
let responseData = [{
name: "name",
appRoles: [{
code: "roleOne",
shouldDisplay: true
}, {
code: "roleTwo",
shouldDisplay: false
}]
}];
let output = responseData.map(o => Object.assign({}, o, {
appRoles: o.appRoles.filter(r => r.shouldDisplay)
}));
console.log(responseData);
console.log(output);
Code explanation -
map
The map function iterates over the whole array and modifying the each item as specified this should be self evident.
Object.assign
This could be the tricky part -
o=>Object.assign({}, o, {appRoles: o.appRoles.filter(r=>r.shouldDisplay)})
From the docs Object.assign is used to copy values from the object.
The first argument {} causes a new object to be created.
The second argument o causes all props from the object o to be copied in the newly created object.
Now, note that we need to modify the appRoles property and keep only those roles which have shouldDisplay as true. That's exactly what the third argument does. It modifies the appRoles property and gives it the new value.
filter
Now the code -
o.appRoles.filter(r=>r.shouldDisplay)
should not be too difficult.
Here we keep only those roles which meet our criterion (namely shouldDisplay should be true)
If you look at the filter function, it expects the callback value to return a boolean value on whose basis it determines whether value has to be kept or not.
So the following code is not even required,
o.appRoles.filter(r=>r.shouldDisplay===true)
This is enough,
o.appRoles.filter(r=>r.shouldDisplay)
There's some missing information in the question. I'll assume the following:
filteredApps should only contain items where there's at least one appRole for display
it is OK if responseData and filteredApps contains references to the same team objects
there are no other references to team objects that need to keep the original data unaffected
As such, you can reduce your code down to this:
let filteredApps = responseData.filter(team =>
(team.appRoles = team.appRoles.filter(role => role.shouldDisplay)).length;
);
The result will be that each team will have only the .shouldDisplay members in its appRoles, and filteredApps will only have teams with at least one appRole with shouldDisplay being true.
You could build a new array with only part who are valid chinldren elements.
let responseData = [{ name: "name", appRoles: [{ code: "roleOne", shouldDisplay: true }, { code: "roleTwo", shouldDisplay: false }] }],
result = responseData.reduce((r, a) => {
var t = a.appRoles.filter(o => o.shouldDisplay);
if (t.length) {
r.push(Object.assign({}, a, { appRoles: t }));
}
return r;
}, []);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Categories

Resources