I am using Angular 13 and I have an array of objects like this:
[{
"name": "Operating System",
"checkedCount": 0,
"children": [{
"name": "Linux",
"value": "Redhat",
"checked": true
},
{
"name": "Windows",
"value": "Windows 10"
}
]
},
{
"name": "Software",
"checkedCount": 0,
"children": [{
"name": "Photoshop",
"value": "PS",
"checked": true
},
{
"name": "Dreamweaver",
"value": "DW"
},
{
"name": "Fireworks",
"value": "FW",
"checked": true
}
]
}
]
I would like to loop through the array, check if each object has a children array and it in turn has a checked property which is set to true, then I should update the checkedCount in the parent object. So, result should be like this:
[{
"name": "Operating System",
"checkedCount": 1,
"children": [{
"name": "Linux",
"value": "Redhat",
"checked": true
},
{
"name": "Windows",
"value": "Windows 10"
}
]
},
{
"name": "Software",
"checkedCount": 2,
"children": [{
"name": "Photoshop",
"value": "PS",
"checked": true
},
{
"name": "Dreamweaver",
"value": "DW"
},
{
"name": "Fireworks",
"value": "FW",
"checked": true
}
]
}
]
I tried to do it this way in angular, but this is in-efficient and results in an error saying this.allFilters[i].children[j] may be undefined. So, looking for an efficient manner to do this.
for(let j=0;i<this.allFilters[i].children.length; j++) {
if (Object.keys(this.allFilters[i].children[j]).length > 0) {
if (Object.prototype.hasOwnProperty.call(this.allFilters[i].children[j], 'checked')) {
if(this.allFilters[i].children[j].checked) {
this.allFilters[i].checkedCount++;
}
}
}
}
Use a nested for loop to check all the children. If checked is truthy, increment the count of the parent. You don't need to check if parent.children has any elements since if there are no elements the loop won't run anyways.
// minified data
const data = [{"name":"Operating System","checkedCount":0,"children":[{"name":"Linux","value":"Redhat","checked":!0},{"name":"Windows","value":"Windows 10"}]},{"name":"Software","checkedCount":0,"children":[{"name":"Photoshop","value":"PS","checked":!0},{"name":"Dreamweaver","value":"DW"},{"name":"Fireworks","value":"FW","checked":!0}]}];
for (const parent of data) {
for (const child of parent.children) {
if (child.checked) parent.checkedCount++;
}
}
console.log(data);
No need to complicate it like that, you just need to check checked property in children.
data.forEach((v) => {
v.children.forEach((child) => {
if (child.checked) {
v.checkedCount++;
}
});
});
Using filter + length on children array should do the job:
const data = [{"name":"Operating System","checkedCount":null,"children":[{"name":"Linux","value":"Redhat","checked":true},{"name":"Windows","value":"Windows 10"}]},{"name":"Software","checkedCount":null,"children":[{"name":"Photoshop","value":"PS","checked":true},{"name":"Dreamweaver","value":"DW"},{"name":"Fireworks","value":"FW","checked":true}]}];
data.forEach(itm => {
itm.checkedCount = itm.children?.filter(e => e.checked === true).length ?? 0;
});
console.log(input);
I would suggest going functional.
Using map
const children = arr.map(obj => obj.children);
const result = children.map((child, idx) => {
const checkedCount = child.filter(obj => obj.checked)?.length;
return {
...arr[idx],
checkedCount
};
});
console.log(result)
or using forEach
const result = [];
const children = arr.map(obj => obj.children);
children.forEach((child, idx) => {
const checkedCount = child.filter(obj => obj.checked)?.length;
result[idx] = {
...arr[idx],
checkedCount
};
});
console.log(result)
I need to filter and map both, array items and each item's child array items while checking whether the to be filtered items have certain properties which meet certain criteria.
What are good array method based approaches for filtering and mapping nested array items?
Required Properties
"productName", "productCategory", "content", "id"
AND also if status inside productImages is not Linked or Existing
Sample Data
[
{
"productName": null,
"productCategory": null,
"productImages": [
{
"content": "1",
"id": null,
"data": null,
"file": null,
"url": null,
"status": "Existing"
},
{
"content": "",
"id": "234",
"data": "test data",
"file": "test file",
"url": null,
"status": "Existing"
}
]
},
{
"productName": null,
"productCategory": "hello category",
"productImages": [
{
"content": "1",
"id": null,
"data": null,
"file": null,
"url": null,
"status": "Existing"
},
{
"content": "",
"id": "234",
"data": "test data",
"file": "test file",
"url": null,
"status": "Existing"
}
]
},
{
"productName": "test product",
"productCategory": "test category",
"productImages": [
{
"content": "1",
"id": "123",
"data": "test data",
"file": "test file",
"url": null,
"status": "Linked"
},
{
"content": "2",
"id": "234",
"data": "test data",
"file": "test file",
"url": "test",
"status": "Linked"
}
]
},
{
"productName": "new product",
"productCategory": "new category",
"productImages": [
{
"content": "2",
"id": "32332",
"data": "test data",
"file": "test file",
"url": "test",
"status": "new"
}
]
}
]
Expected Output
[
{
"productIndex": 3,
"name": "new product",
"category": "new category",
"filteredImages": [
{
"newcontent": "2",
"id": "32332",
},
]
},
]
Code
const filteredProducts = products.filter((element) => element.productName && element.productCategory);
You can do it with one for-of loop:
const incomplete_items = [];
const new_data = []
for (const [index, item] of data.entries()) {
if(item.productName && item.productCategory && item.productImages.every((image) => image.id && image.content && !['Existing', 'Linked'].includes(image.status))){
new_data.push({
productIndex: index,
name: item.productName,
category: item.productCategory,
filteredImages: item.productImages.map((image) => ({ newcontent: image.content, id: image.id, }))
})
} else {
incomplete_items.push(index);
}
}
Working example
const data = [
{
"productName": null,
"productCategory": null,
"productImages": [
{ "content": "1", "id": null, "data": null, "file": null, "url": null, "status": "Existing" },
{ "content": "","id": "234","data": "test data", "file": "test file", "url": null, "status": "Existing" }
]
},
{
"productName": null,
"productCategory": "hello category",
"productImages": [
{ "content": "1", "id": null,"data": null, "file": null, "url": null, "status": "Existing"},
{ "content": "", "id": "234","data": "test data","file": "test file", "url": null, "status": "Existing" }
]
},
{
"productName": "test product",
"productCategory": "test category",
"productImages": [
{ "content": "1", "id": "123", "data": "test data", "file": "test file", "url": null, "status": "Linked" },
{ "content": "2", "id": "234", "data": "test data", "file": "test file", "url": "test","status": "Linked" }
]
},
{
"productName": "new product",
"productCategory": "new category",
"productImages": [
{ "content": "2","id": "32332", "data": "test data", "file": "test file", "url": "test", "status": "new" }
]
}
]
const incomplete_items = [];
const new_data = []
for (const [index, item] of data.entries()) {
if(item.productName && item.productCategory && item.productImages.every((image) => image.id && image.content && !['Existing', 'Linked'].includes(image.status))){
new_data.push({
productIndex: index,
name: item.productName,
category: item.productCategory,
filteredImages: item.productImages.map((image) => ({ newcontent: image.content, id: image.id, }))
})
} else {
incomplete_items.push(index);
}
}
if(incomplete_items.length) console.log(`Items ${incomplete_items} were missing required properties.`);
console.log(new_data);
You can use reduce for this.
Thank you Peter Seliger for pointing me to the double reduce. In terms of readability, do you think it's preferred to use 2 reducers instead of a filter().map() inside the outer reducer?
const sampleData = [{productName:null,productCategory:null,productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:null,productCategory:"hello category",productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:"test product",productCategory:"test category",productImages:[{content:"1",id:"123",data:"test data",file:"test file",url:null,status:"Linked"},{content:"2",id:"234",data:"test data",file:"test file",url:"test",status:"Linked"}]},{productName:"new product",productCategory:"new category",productImages:[{content:"2",id:"32332",data:"test data",file:"test file",url:"test",status:"new"}]}];
const expectedData = sampleData.reduce((acc, val, i) => {
// check if productName and productCategory are available
if (val.productName && val.productCategory) {
// get images that match the criteria and return object in desired format
const filteredImages = val.productImages.reduce(
(matchingImages, { content, id, status }) => {
if (content && id && status !== "Existing" && status !== "Linked") {
matchingImages.push({ newcontent: content, id });
}
return matchingImages;
},
[]
);
// if images are available, all conditions are met
if (filteredImages.length > 0) {
acc.push({
productIndex: i,
name: val.productName,
category: val.productCategory,
filteredImages,
});
}
}
return acc;
}, []);
console.log(expectedData);
.as-console-wrapper { min-height: 100%!important; top: 0; }
A straightforward approach would be based on just two reducer functions, each being properly named according to its implemented purpose, where the outer reduce task iterates the products and the inner task iterates each product's related images.
Thus one achieves both within each reduce task, the filtering and the property mapping for the expected result items.
const input = [{productName:null,productCategory:null,productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:null,productCategory:"hello category",productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:"test product",productCategory:"test category",productImages:[{content:"1",id:"123",data:"test data",file:"test file",url:null,status:"Linked"},{content:"2",id:"234",data:"test data",file:"test file",url:"test",status:"Linked"}]},{productName:"new product",productCategory:"new category",productImages:[{content:"2",id:"32332",data:"test data",file:"test file",url:"test",status:"new"}]}];
function collectNonMatchingImageItem(result, image) {
const { id = null, content = null, status = null } = image;
if (
// loose truthyness comparison ... 1st stage filter (valid image) ...
String(id ?? '').trim() &&
String(content ?? '').trim() &&
String(status ?? '').trim() &&
// ... 2nd stage filter (non matching valid image item) ...
!(/^linked|existing$/i).test(status)
) {
// ... and mapping at once.
result.push({
id,
newcontent: content,
});
}
return result;
}
function collectValidProductWithNonMatchingImages(result, product, idx) {
const {
productName = null,
productCategory = null,
productImages = [],
} = product;
// loose truthyness comparison ... 1st stage filter (valid product) ...
if (
String(productName ?? '').trim() &&
String(productCategory ?? '').trim()
) {
const nonMatchingItems = productImages
.reduce(collectNonMatchingImageItem, []);
// 2nd stage filter (valid product with non matching valid images) ...
if (nonMatchingItems.length >= 1) {
// ... and mapping at once.
result.push({
productIndex: idx,
name: productName,
category: productCategory,
filteredImages: nonMatchingItems,
});
}
}
return result;
}
const result = input
.reduce(collectValidProductWithNonMatchingImages, []);
console.log({ result });
.as-console-wrapper { min-height: 100%!important; top: 0; }
Edit
"... How do you compare the old and new array. I wanted to output a warning if it isn't complete" – Joseph
The above approach shows its flexibility if it comes to the integration of the new requirement ...
const input = [{productName:null,productCategory:null,productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:null,productCategory:"hello category",productImages:[{content:"1",id:null,data:null,file:null,url:null,status:"Existing"},{content:"",id:"234",data:"test data",file:"test file",url:null,status:"Existing"}]},{productName:"test product",productCategory:"test category",productImages:[{content:"1",id:"123",data:"test data",file:"test file",url:null,status:"Linked"},{content:"2",id:"234",data:"test data",file:"test file",url:"test",status:"Linked"}]},{productName:"new product",productCategory:"new category",productImages:[{content:"2",id:"32332",data:"test data",file:"test file",url:"test",status:"new"}]}];
function collectNonMatchingImageItem(collector, image, idx) {
const { id = null, content = null, status = null } = image;
// loose truthyness comparison ... 1st stage filter (valid image) ...
if (
String(id ?? '').trim() &&
String(content ?? '').trim() &&
String(status ?? '').trim()
) {
// ... 2nd stage filter (non matching valid image item) ...
if (
!(/^linked|existing$/i).test(status)
) {
// ... and mapping at once.
collector.result.push({
id,
newcontent: content,
});
collector.nonMatchingAt.push(idx);
}
} else {
collector.invalidAt.push(idx);
}
return collector;
}
function collectValidProductWithNonMatchingImages(collector, product, idx) {
const {
productName = null,
productCategory = null,
productImages = [],
} = product;
// loose truthyness comparison ... 1st stage filter (valid product) ...
if (
String(productName ?? '').trim() &&
String(productCategory ?? '').trim()
) {
const {
result: nonMatchingItems,
nonMatchingAt,
invalidAt,
} = productImages.reduce(
collectNonMatchingImageItem,
{ result: [], nonMatchingAt: [], invalidAt: [] },
);
// report related variables.
let isPlural, itemPhrase, indexPhrase, imageReport;
// 2nd stage filter (valid product with non matching valid images) ...
if (nonMatchingItems.length >= 1) {
// ... and mapping at once.
collector.result.push({
productIndex: idx,
name: productName,
category: productCategory,
filteredImages: nonMatchingItems,
});
// create and collect report item.
isPlural = (nonMatchingAt.length >= 2);
itemPhrase = `item${ isPlural ? 's' : '' }`;
indexPhrase = `${ isPlural ? 'indices' : 'index' }`;
imageReport = `with non matching valid image ${ itemPhrase } at ${ indexPhrase } ${ nonMatchingAt }`;
collector.report.push(
`Valid product item at index ${ idx } ${ imageReport }.`
);
}
if (invalidAt.length >= 1) {
// create and collect report item.
isPlural = (invalidAt.length >= 2);
itemPhrase = `item${ isPlural ? 's' : '' }`;
indexPhrase = `${ isPlural ? 'indices' : 'index' }`;
imageReport = `with invalid image ${ itemPhrase } at ${ indexPhrase } ${ invalidAt }`;
collector.report.push(
`Valid product item at index ${ idx } ${ imageReport }.`
);
}
} else {
// create and collect report item.
collector.report.push(
`Invalid product item at index ${ idx }.`
);
}
return collector;
}
const {
result,
report,
} = input.reduce(
collectValidProductWithNonMatchingImages,
{ result: [], report: [] },
);
console.log({ result, report });
.as-console-wrapper { min-height: 100%!important; top: 0; }