Set the boolean property inside all objects with matched name - javascript

I have a nested object that looks like this
const test = {
cat1: {
id: "c1",
name: "category1",
items: [
{
itemName: "item1",
points: 1,
used: true
},
{
itemName: "item2",
points: 3,
used: false
},
{
itemName: "item3",
points: 5,
used: true
}
]
},
cat2: {
id: "c2",
name: "category2",
items: [
{
itemName: "item4",
points: 7,
used: true
},
{
itemName: "item5",
points: 9,
used: false
}
]
},
cat3: {
id: "c3",
name: "category3",
items: [
{
itemName: "item6"
}
]
}
};
These items are then drawn as checkbox with used property as its checked value. So whenever I click on Select All all the items having used property inside the objects should be set to true. When Unselect All, all the items used property should be set to false. I prefer having just one function that basically takes type as Select All or Unselect All as key and a category name so that items under that name will be set to either true or false
Here is my try
function handleOperation(id, type){
const output = Object.fromEntries(
Object.entries(test)
.map(([k, { items, ...rest }]) => [
k,
{
...rest,
items : type === 'Select All' ?
items[k] = items[k].map(item => {
item.used = true;
return item;
}) :
items[k] = items[k].map(item => {
item.used = false;
return item;
})
}
]);
);
return output;
}
console.log(handleOperation('category1', 'Select All'));
console.log(handleOperation('category2', 'Unselect All'));
So when I pass handleOperation('category1', 'Select All')
It should give me, since category1 items should be updated to true
const test = {
cat1: {
id: "c1",
name: "category1",
items: [
{
itemName: "item1",
points: 1,
used: true
},
{
itemName: "item2",
points: 3,
used: true
},
{
itemName: "item3",
points: 5,
used: true
}
]
},
cat2: {
id: "c2",
name: "category2",
items: [
{
itemName: "item4",
points: 7,
used: true
},
{
itemName: "item5",
points: 9,
used: false
}
]
},
cat3: {
id: "c3",
name: "category3",
items: [
{
itemName: "item6"
}
]
}
}

Since you want to clone the objects (although you could avoid cloning the ones that don't change; probably an unnecessary optimization), you're on the right track with Object.fromEntries, Object.entries, and map. But you seem to be using the wrong property names, and the code doesn't have to be as complicated as shown.
function handleOperation(type) {
// Get a flag for `type`
const used = type === "Select All";
// Build the updated objects
return Object.fromEntries(
Object.entries(test).map(([key, { items, ...rest }]) => [
key,
{
// Copy the rest of this object
...rest,
// Copy its array while mapping the objects
items: items.map((item) => ({
// Copy `items`'s properties
...item,
// Set the flag from our `used `variable
used,
})),
},
])
);
}
const test = {
cat1: {
id: "c1",
name: "category1",
items: [
{
itemName: "item1",
points: 1,
used: true,
},
{
itemName: "item2",
points: 3,
used: false,
},
{
itemName: "item3",
points: 5,
used: true,
},
],
},
cat2: {
id: "c2",
name: "category2",
items: [
{
itemName: "item4",
points: 7,
used: true,
},
{
itemName: "item5",
points: 9,
used: false,
},
],
},
cat3: {
id: "c3",
name: "category3",
items: [
{
itemName: "item6",
},
],
},
};
function handleOperation(type) {
// Get a flag for `type`
const used = type === "Select All";
// Build the updated objects
return Object.fromEntries(
Object.entries(test).map(([key, { items, ...rest }]) => [
key,
{
// Copy the rest of this object
...rest,
// Copy its array while mapping the objects
items: items.map((item) => ({
// Copy `items`'s properties
...item,
// Set the flag from our `used `variable
used,
})),
},
])
);
}
console.log("Select All:");
console.log(handleOperation("Select All"));
console.log("Unselect All:");
console.log(handleOperation("Unselect All"));
.as-console-wrapper {
max-height: 100% !important;
}
In a comment you've asked:
Thanks a ton. If I want to modify items array with category name being passed. What could be modified? Something like calling handleOperation('category1', 'Select All')), So that only category1 items objects are set to true
One way we could do that is to just avoid calling map on items if the name of the category doesn't match, using the existing items array:
function handleOperation(categoryName, type) {
// Get a flag for `type`
const used = type === "Select All";
// Build the updated objects
return Object.fromEntries(
Object.entries(test).map(([key, { name, items, ...rest }]) => [
key,
{
// Copy the name
name,
// Copy the rest of this object
...rest,
// If the name doesn't match, reuse `items`; if it does match,
// copy its array while mapping the objects
items:
name !== categoryName
? items
: items.map((item) => ({
// Copy `items`'s properties
...item,
// Set the flag from our `used `variable
used,
})),
},
])
);
}
const test = {
cat1: {
id: "c1",
name: "category1",
items: [
{
itemName: "item1",
points: 1,
used: true,
},
{
itemName: "item2",
points: 3,
used: false,
},
{
itemName: "item3",
points: 5,
used: true,
},
],
},
cat2: {
id: "c2",
name: "category2",
items: [
{
itemName: "item4",
points: 7,
used: true,
},
{
itemName: "item5",
points: 9,
used: false,
},
],
},
cat3: {
id: "c3",
name: "category3",
items: [
{
itemName: "item6",
},
],
},
};
function handleOperation(categoryName, type) {
// Get a flag for `type`
const used = type === "Select All";
// Build the updated objects
return Object.fromEntries(
Object.entries(test).map(([key, { name, items, ...rest }]) => [
key,
{
// Copy the name
name,
// Copy the rest of this object
...rest,
// If the name doesn't match, reuse `items`; if it does match,
// copy its array while mapping the objects
items:
name !== categoryName
? items
: items.map((item) => ({
// Copy `items`'s properties
...item,
// Set the flag from our `used `variable
used,
})),
},
])
);
}
console.log("category1 - Select All:");
console.log(handleOperation("category1", "Select All"));
console.log("category1 - Unselect All:");
console.log(handleOperation("category1", "Unselect All"));
.as-console-wrapper {
max-height: 100% !important;
}
But that unnecessarily copies category objects. So let's avoid that as well:
function handleOperation(categoryName, type) {
// Get a flag for `type`
const used = type === "Select All";
// Build the updated objects
return Object.fromEntries(
Object.entries(test).map(([key, category]) => {
if (category.name !== categoryName) {
return [key, category];
}
const { name, items, ...rest } = category;
return [
key,
{
// Copy the name
name,
// Copy the rest of this object
...rest,
// Copy its array while mapping the objects
items: items.map((item) => ({
// Copy `items`'s properties
...item,
// Set the flag from our `used `variable
used,
})),
},
];
})
);
}
const test = {
cat1: {
id: "c1",
name: "category1",
items: [
{
itemName: "item1",
points: 1,
used: true,
},
{
itemName: "item2",
points: 3,
used: false,
},
{
itemName: "item3",
points: 5,
used: true,
},
],
},
cat2: {
id: "c2",
name: "category2",
items: [
{
itemName: "item4",
points: 7,
used: true,
},
{
itemName: "item5",
points: 9,
used: false,
},
],
},
cat3: {
id: "c3",
name: "category3",
items: [
{
itemName: "item6",
},
],
},
};
function handleOperation(categoryName, type) {
// Get a flag for `type`
const used = type === "Select All";
// Build the updated objects
return Object.fromEntries(
Object.entries(test).map(([key, category]) => {
if (category.name !== categoryName) {
return [key, category];
}
const { name, items, ...rest } = category;
return [
key,
{
// Copy the name
name,
// Copy the rest of this object
...rest,
// Copy its array while mapping the objects
items: items.map((item) => ({
// Copy `items`'s properties
...item,
// Set the flag from our `used `variable
used,
})),
},
];
})
);
}
console.log("category1 - Select All:");
console.log(handleOperation("category1", "Select All"));
console.log("category1 - Unselect All:");
console.log(handleOperation("category1", "Unselect All"));
.as-console-wrapper {
max-height: 100% !important;
}

Related

Advanced filtering nested elements in TypeScript/JavaScript

Given the following structure and data:
interface GrandChild {
id: number,
values: Array<string>,
}
interface Child {
id: number,
subItems: Array<GrandChild>
}
interface Foo {
items: Array<Child>
}
const data: Foo = {
items: [
{ id: 1, subItems: [ { id: 10, values: ['10', '100'] }, { id: 11, values: ['11', '110', '1100'] } ] },
{ id: 2, subItems: [ { id: 20, values: ['REMOVE', 'REMOVE'] }, { id: 21, values: ['REMOVE'] } ] },
{ id: 3, subItems: [ { id: 30, values: ['REMOVE'] }, { id: 31, values: ['REMOVE'] }, { id: 32, values: ['REMOVE', '32'] } ] },
]
};
How can I use the Array's methods (filter, map, some, etc.) to achieve the following result?
const expected: Foo = {
items: [
{ id: 1, subItems: [ { id: 10, values: ['10', '100'] }, { id: 11, values: ['11', '110', '1100'] } ] },
{ id: 3, subItems: [ { id: 32, values: ['32'] } ] },
]
}
So far, I filtered the resulting data, removing the undesired elements, as following:
const filteredData: Foo = {
...data,
items: data.items.map(item => ({
...item,
subItems: item.subItems.map(subItem => ({
...subItem,
values: subItem.values.filter(value => value !== 'REMOVE')
}))
}))
}
Resulting:
{
items: [
{ id: 1, subItems: [ { id: 10, values: ['10', '100'] }, { id: 11, values: ['11', '110', '1100'] } ] },
{ id: 2, subItems: [ { id: 20, values: [] }, { id: 21, values: [] } ] },
{ id: 3, subItems: [ { id: 30, values: [] }, { id: 31, values: [] }, { id: 32, values: ['32'] } ] },
]
};
But, I cannot figure a way out to remove the empty subItems elements without looping through the result.
You can check online the above code here.
If you really want to do it just with filter and map, add a filter after each of your maps to remove subItems that have an empty values array and to remove items that have an empty subItems array:
const filteredData = {
...data,
items: data.items
.map((item) => ({
...item,
subItems: item.subItems
.map((subItem) => ({
...subItem,
values: subItem.values.filter((value) => value !== "REMOVE"),
}))
.filter(({ values }) => values.length > 0), // ***
}))
.filter(({subItems}) => subItems.length > 0), // ***
};
But:
When I have map followed by filter, I always ask myself if the data is large enough that I should avoid making multiple passes through it.
When I'm doing lots of nesting of map calls and such, I always ask myself if it would be clearer when reading the code later to use simpler, smaller loops.
Here's what you might do if answering "yes" to either or both of those questions:
const filteredData: Foo = {
...data,
items: [],
};
for (const item of data.items) {
const subItems: Array<GrandChild> = [];
for (const subItem of item.subItems) {
const values = subItem.values.filter((value) => value !== "REMOVE");
if (values.length) {
subItems.push({
...subItem,
values,
});
}
}
if (subItems.length > 0) {
filteredData.items.push({
...item,
subItems,
});
}
}

How to change value in array with objects in JS

I have an array and want to change name in object { id: 4, name: 'name4' } to 'name6'
const example = [
{
id: '1234',
desc: 'sample1',
items: [
{ id: 1, name: 'name1' },
{ id: 2, name: 'testItem2' }
]
},
{
id: '3456',
desc: 'sample2',
items: [
{ id: 4, name: 'name4' },
{ id: 5, name: 'testItem5' }
]
},
I try in this way but it isn't working
const name = 'name4';
const result = example?.forEach((group) =>
group.items.forEach((item) =>
if (item.name === name) {
return item.name === 'name6';
}
return null;
})
);
The for...of statement is my recommendation for readability and loop optimisation.
const example = [
{
id: '1234',
desc: 'sample1',
items: [
{ id: 1, name: 'name1' },
{ id: 2, name: 'testItem2' },
],
},
{
id: '3456',
desc: 'sample2',
items: [
{ id: 4, name: 'name4' },
{ id: 5, name: 'testItem5' },
],
},
];
const oldName = 'name4';
const newName = 'name6';
for (const group of example) {
for (const item of group.items) {
if (item.name === oldName) {
item.name === newName;
break
}
}
}
You could even go a step further and terminate the outer loop with a label if you only need to change the name in a single group.
outerLoop: for (const group of example) {
for (const item of group.items) {
if (item.name === oldName) {
item.name === newName;
break outerLoop;
}
}
}
Hope this helps.
You could either change the value by simply assigning a new value.
example[1].items[0].name = 'name6'
But you can also iterate through all items and search for the name you want to change. I created a function that goes through an array and loops over its nested items arrays searching for any given name (targetName) and replacing it with a new one (newName):
function changeName(array, targetName, newName) {
// Loop through the elements of array
array.forEach((element) => {
// Check each item: change the name if it matches the target
element.items.forEach((item) => {
if (item.name === targetName) item.name = newName;
});
});
}
// This function will check example array and change
// every name that has a value 'name4' into 'name6'
changeName(example, "name4", "name6");
forEach doesn't return any value.
Instead of return item.name === 'name6' you can simply set new value to item.name.
Why not like this?
const example = [{
id: '1234',
desc: 'sample1',
items: [{
id: 1,
name: 'name1'
},
{
id: 2,
name: 'testItem2'
}
]
},
{
id: '3456',
desc: 'sample2',
items: [{
id: 4,
name: 'name4'
},
{
id: 5,
name: 'testItem5'
}
]
},
]
example[1].items[0].name = 'name6'
console.log(example)

Running a mapping task what is the correct 2nd array method for processing one of the newly created item's values?

I have an array containing objects:
let sportTag = [
{ id: 1, name: 'FOOTBALL', found: false },
{ id: 2, name: 'TENNIS' , found: false },
{ id: 3, name: 'BASKET' , found: false },
]
I have also have another array containing objects and for every object a field (sports) that is an array:
let person = [{
id: 1,
name: "Mark",
age: 23,
sports: ["volleyball", "rugby", "tennis"],
}, {
id: 2,
name: "Rupert",
age: 40,
sports: ["golf"],
}, {
id: 3,
name: "John",
age: 31,
sports: ["football", "golf", "rugby", "tennis"],
}]
I would like to change sportTag found field to true when sportTag name is equal to every person sport.
I tried with a nested map
const result = sportTag.map(st => {
person.map(p => {
p.sports.map(s => {
if (st.name.toLocaleUpperCase() === s.toLocaleUpperCase()) {
return {
...st, found: true
}
}
return s
})
return p
})
return st
})
console.log(sportTag)
//OUTPUT
// { id: 1, name: 'FOOTBALL', found: false },
// { id: 2, name: 'TENNIS' , found: false },
// { id: 3, name: 'BASKET' , found: false }
console.log(result)
//OUTPUT
// { id: 1, name: 'FOOTBALL', found: false },
// { id: 2, name: 'TENNIS' , found: false },
// { id: 3, name: 'BASKET' , found: false }
Why are the changes not reflected in the result? I expect the output to be:
{ id: 1, name: 'FOOTBALL', found: true },
{ id: 2, name: 'TENNIS' , found: true },
{ id: 3, name: 'BASKET' , found: false }
The problem with your code is that you are always returning st for each iteration of the first map, so you get the original values.
You probably want something like this:
const result = sportTag.map(st => {
const foundSport = person.find(p =>
p.sports.find(s => st.name.toLocaleUpperCase() === s.toLocaleUpperCase())
);
return foundSport
? { ...st, found: true }
: st;
});
console.log(sportTag)
// { id: 1, name: 'FOOTBALL', found: false },
// { id: 2, name: 'TENNIS', found: false },
// { id: 3, name: 'BASKET', found: false }
console.log(result)
// { id: 1, name: 'FOOTBALL', found: true },
// { id: 2, name: 'TENNIS', found: true },
// { id: 3, name: 'BASKET', found: false }
From the above comment ...
The OP already mentions in the description of the problem the correct way of achieving what the OP wants ... "I would like to change [the] sportTag's found field to true when [the] sportTag's name [value] is equal to every [any/some] person's sport [item]." ... thus the OP does not need to implement a nested, twice map but a map/some task.
But (especially for a bigger amount of data) instead of following the above suggested approach which within every map iteration additionally iterates again with every nested some task, one could choose a lookup based approach which works with a Map instance. The mapping task itself will be very simple. For the latter one could even choose an implementation which makes the mapping agnostic to the current lookup's variable/constant name since one would provide its reference as the map method's 2nd thisArg parameter.
One of cause could implement the lookup creation with less iteration cycles. But since it is done once it will never become the performance bottleneck.
function createLookupOfAnyPracticedSport(persons) {
return new Map(
// (5) create a map as lookup for unique sport items.
Array
// (3) create array from set of step (2)
.from(
new Set(
// (2) create a set of unique sport
// items/values as of step (1)
persons
// (1) concatenate array of all `sports`
// practiced by any person.
.reduce((result, { sports }) =>
result.concat(sports), []
)
)
)
// (4) sanitize and map the unique sport items/values
// in order to qualify as entries for step (5) ...
.map(sport => [sport.toLocaleUpperCase(), true])
);
}
function createUpToDateSportTagFromBoundSports(tagItem) {
const allSportsLookup = this;
// create (updated) shallow copy of the original
// sport tag item in order to not directly mutate
// such an item's original reference.
return {
...tagItem,
found: allSportsLookup
.has(tagItem.name.toLocaleUpperCase())
};
}
const personList = [{
id: 1, name: "Mark", age: 23,
sports: ["volleyball", "rugby", "tennis"],
}, {
id: 2, name: "Rupert", age: 40,
sports: ["golf"],
}, {
id: 3, name: "John", age: 31,
sports: ["football", "golf", "rugby", "tennis"],
}];
const sportTagList = [{
id: 1, name: 'FOOTBALL', found: false,
}, {
id: 2, name: 'TENNIS', found: false,
}, {
id: 3, name: 'BASKET', found: false,
}];
const mappedTagList = sportTagList
.map(
createUpToDateSportTagFromBoundSports,
createLookupOfAnyPracticedSport(personList),
);
console.log({
mappedTagList,
sportTagList,
personList,
});
console.log(
'entries of any practiced sport ...',
[...createLookupOfAnyPracticedSport(personList).entries()],
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
let person = [{
id: 1, name: "Mark", age: 23,
sports: ["volleyball", "rugby", "tennis"],
}, {
id: 2, name: "Rupert", age: 40,
sports: ["golf"],
}, {
id: 3, name: "John", age: 31,
sports: ["football", "golf", "rugby", "tennis"],
}];
let sportTag = [{
id: 1, name: 'FOOTBALL', found: false,
}, {
id: 2, name: 'TENNIS', found: false,
}, {
id: 3, name: 'BASKET', found: false,
}];
sportTag.forEach((elem, index, array) => {
person.forEach((el, i, arr) => {
if (person[i].sports.indexOf(sportTag[index].name.toLocaleLowerCase()) != -1) {
sportTag[index].found = true;
}
});
});
console.log(sportTag);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Filter an object iside an array

I need to delete entire object that do not have passed
here is the array
const array = [{
course: 1,
list: [{
id: 1,
name: "john",
code: true
},
{
id: 1,
name: "maria",
code: true
},
]
},
{
course: 2,
list: [{
id: 3,
name: "rose"
},
{
id: 4,
name: "mark",
code: true
}
]
}
]
That i need is remove obj that not have code:true, and get this
const array = [{
course: 1,
list: [{
id: 1,
name: "john",
code: true
}, ]
},
{
course: 2,
list: [{
id: 1,
name: "mark",
code: true
}]
}
]
I tried to make a map inside a filter, but it does not work at all
const remove = array.filter(function(lines) {
return lines.map(line => line.list.map(list => list.code))
});
You can map through the array, then copy all properties of the specific item and separately do the filtering on the list attribute.
const array = [{
course: 1,
list: [{
id: 1,
name: "john",
code: true
},
{
id: 1,
name: "maria",
code: true
},
]
},
{
course: 2,
list: [{
id: 3,
name: "rose"
},
{
id: 4,
name: "mark",
code: true
}
]
}
]
const filter = arr => arr.map(arrItem => ({
...arrItem,
list: arrItem.list.filter( listItem => listItem.code )
})
)
console.log( filter(array) )
const filtered = [];
arr.forEach(item => {
const list = item.list.filter(listItem => listItem.code);
if(list.length > 0) {
filter.push({ ...item, list });
}
});
This approach will only add items to the filtered output array if the list contains any items after filtering out those with code: false. To include them anyway, you could do:
const filtered = arr.map(item => ({
...item,
list: item.list.filter(listItem => listItem.code)
});

How to recursively transform an array of nested objects into array of flat objects?

I have the following array of deeply nested objects:
const data = [
{
name: "foo",
children:[
{
count: 1,
name: "A"
},
{
count: 2,
name: "B"
}
]
},
{
name: "bar",
children: [
{
count: 3,
name: "C",
children: [
{
count: 4,
name: "D"
}
]
}
]
}
]
The way I'd like to transform this would be such as:
const expectedStructure = [
{
count: 1,
name: "A",
label: "foo = A"
},
{
count: 2,
name: "B",
label: "foo = B"
},
{
count: 3,
name: "C",
label: "bar = C"
},
{
count: 4,
name: "D",
label: "bar = D"
}
]
I created recursive function that transforms nested array into array of flat objects.
Here's my code:
function getChildren(array, result=[]) {
array.forEach(({children, ...rest}) => {
result.push(rest);
if(children) {
getChildren(children, result);
}
});
return result;
}
And here's output I get:
[ { name: 'foo' },
{ count: 1, name: 'A' },
{ count: 2, name: 'B' },
{ name: 'bar' },
{ count: 3, name: 'C' },
{ count: 4, name: 'D' } ]
The problem is that I need to add label field to every object in my output array, and I can't find a solution without iterating multiple times through the final array to make desired transformation. How to properly insert label field without hugely augmenting complexity of the function?
Check each iteration whether the current item is a "parent" item, and reassign label if it is.
const data = [{name:"foo",children:[{count:1,name:"A"},{count:2,name:"B"}]},{name:"bar",children:[{count:3,name:"C",children:[{count:4,name:"D"}]}]}];
function getChildren(array, result = [], label = "") {
array.forEach(({ children, name, count }) => {
if (!label || name[1]) {
label = `${name} = `;
}
if (count) {
result.push({ count, name, label: label + name });
}
if (children) {
getChildren(children, result, label);
}
});
return result;
}
const res = getChildren(data);
console.log(res);
You can use a different function for the nested levels, so you can pass the top-level name properties down through all those recursion levels.
function getTopChildren(array, result = []) {
array.forEach(({
name,
children
}) => {
if (children) {
getChildren(children, name, result);
}
});
return result;
}
function getChildren(array, name, result) {
array.forEach(({
children,
...rest
}) => {
rest.label = `${name} = ${rest.name}`;
result.push(rest);
if (children) {
getChildren(children, name, result);
}
});
}
const data = [{
name: "foo",
children: [{
count: 1,
name: "A"
},
{
count: 2,
name: "B"
}
]
},
{
name: "bar",
children: [{
count: 3,
name: "C",
children: [{
count: 4,
name: "D"
}]
}]
}
]
console.log(getTopChildren(data));
You can also do this recursively with flatMap based on whether or not a parent has been passed into the recursive call :
const data = [{
name: "foo",
children: [{
count: 1,
name: "A"
},
{
count: 2,
name: "B"
}
]
},
{
name: "bar",
children: [{
count: 3,
name: "C",
children: [{
count: 4,
name: "D"
}]
}]
}
];
function flatten(arr, parent = null) {
return parent
? arr.flatMap(({name, count, children}) => [
{name, count, label: `${parent} = ${name}`},
...flatten(children || [], parent)
])
: arr.flatMap(({name, children}) => flatten(children || [], name));
}
console.log(flatten(data));
Sometimes it's a little easier to reason about the code and write it clearly using generators. You can yield* from the recursive calls:
const data = [{name: "foo",children:[{count: 1,name: "A"},{ count: 2,name: "B"}]},{name: "bar",children: [{count: 3,name: "C",children: [{count: 4,name: "D"}]}]}]
function* flat(input, n){
if (!input) return
if (Array.isArray(input)) {
for (let item of input)
yield* flat(item, n)
}
let _name = n || input.name
if ('count' in input) {
yield { count:input.count, name:input.name, label:`${_name} = ${input.name}`}
}
yield* flat(input.children, _name)
}
let g = [...flat(data)]
console.log(g)
The function returns a generator, so you need to spread it into a list [...flat(data)] if you want a list or iterate over it if you don't need to store the list.

Categories