Javascript, dynamically create an empty object copy during a map - javascript

I'm struggling to find a good solution to process my array of objects.
I have two arrays:
let structure = ["horizontal","vertical","small","small","small"]
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"small","position":2},
{"id":9,"title":"xxxxx","format":"small","position":3},
{"id":11,"title":"xxxxx","format":"small","position":4}]
Edit: Items are more complex than this: it has about 15 attributes...
structure has a dynamic length and is my reference array. When I change structure I must remap the items array changing the format according to structure. So if I change structure to
let structure = ["horizontal","vertical","vertical","vertical","small"]
The array must change to
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"vertical","position":2},
{"id":9,"title":"xxxxx","format":"vertical","position":3},
{"id":11,"title":"xxxxx","format":"small","position":4}]
This can be done with a map.
This is my Vue method, I map the structure and use the function changeStructure I change the format.
methods: {
changeStructure(object,structure) {
object.format = structure
return object
},
updateCoverElements() {
let structure = this.coverTypes[this.currentCoverVersion]
let elements = this.coverElements
let changeStructure = this.changeStructure
let revisedElement = structure.map(function(structure, index) {
return changeStructure(elements[index],structure)
});
console.log(revisedElement);
}
},
But the problem is that, as I told before, structure has a dynamic length.
So when I change to
let structure = ["horizontal","vertical","vertical"]
Item results must be
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"vertical","position":2}]
This is not a problem, if the new structure length has less elements.
But when I change to
let structure = ["horizontal","vertical","vertical","vertical","vertical","vertical","vertical"]
Item results must be
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"vertical","position":2},
{"id":"","title":"","format":"vertical","position":3},
{"id":"","title":"","format":"vertical","position":4},
{"id":"","title":"","format":"vertical","position":5},
{"id":"","title":"","format":"vertical","position":6}]
And here is the problem: I cannot find a good way to dynamically create an object with the same identical structure as other items objects (a copy), with every field empty except for position, the index of the array, and format.

You could use the spread syntax like this. If items has a value at the index, it will overwrite the default id and title values.
let structure = ["horizontal","vertical","vertical","vertical","vertical","vertical","vertical"]
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"vertical","position":2}]
const defaultObj = { id: '', title: '' }
const newItems = structure.map((format, position) => {
return { ...defaultObj, ...items[position], format, position }
})
console.log(newItems)

Just slice off a new copy of items with max structure.length items, then iterate through your new clone of items and set each format attribute. Finally, create new objects for any elements in structure that don't have a corresponding partner in items:
var structure = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']
var items = [
{'id':1,'title':'xxxxx','format':'horizontal','position':0},
{'id':3,'title':'xxxxx','format':'vertical', 'position':1},
{'id':6,'title':'xxxxx','format':'vertical', 'position':2},
];
// update all extant items
var _items = Object.assign([], items.slice(0, structure.length));
_items.forEach(function(i, idx) { i.format = structure[idx] });
// create any new items
for (var i=_items.length; i<structure.length; i++) {
_items.push(Object.assign({}, items[0], {
id: '',
title: '',
position: i,
format: structure[i],
}))
}
console.log(_items)

The best way to solve this problem using map() method would be to organize your implementation functional so that you could call it many times with a different set of data wherever you need (the concept of reusability).
Here is what I have tried. I have defined the function named getAlteredArray(items, structure) which returns the desired array. Note that if you are looking to use map() method means you want a new array (I mean you don't want to alter the passed array).
I have pasted the o/p on the code itself (it is commented out).
// function that creates new array with desired items and returns to the caller
function getAlteredArray(items, structure) {
let newArr = items.map((obj, index) => {
// obj => {"id":1,"title":"xxxxx","format":"horizontal","position":0}
obj["format"] = structure[index]
return obj
})
return newArr
}
// function that creates set of data needs to be passed to getAlteredArray() function
function test() {
let items = [{"id":1,"title":"xxxxx","format":"horizontal","position":0},
{"id":3,"title":"xxxxx","format":"vertical","position":1},
{"id":6,"title":"xxxxx","format":"small","position":2},
{"id":9,"title":"xxxxx","format":"small","position":3},
{"id":11,"title":"xxxxx","format":"small","position":4}]
// TEST 1 - Call getAlteredArray() with `items` & `structure`
let structure = ["horizontal","vertical","vertical","vertical","small"]
let newArr = getAlteredArray(items, structure)
console.log(newArr)
/*
[ { id: 1, title: 'xxxxx', format: 'horizontal', position: 0 },
{ id: 3, title: 'xxxxx', format: 'vertical', position: 1 },
{ id: 6, title: 'xxxxx', format: 'vertical', position: 2 },
{ id: 9, title: 'xxxxx', format: 'vertical', position: 3 },
{ id: 11, title: 'xxxxx', format: 'small', position: 4 } ]
*/
// TEST 2
let structure2 = ["horizontal","vertical","small","small","small"]
let newArr2 = getAlteredArray(items, structure2)
console.log(newArr2)
/*
[ { id: 1, title: 'xxxxx', format: 'horizontal', position: 0 },
{ id: 3, title: 'xxxxx', format: 'vertical', position: 1 },
{ id: 6, title: 'xxxxx', format: 'small', position: 2 },
{ id: 9, title: 'xxxxx', format: 'small', position: 3 },
{ id: 11, title: 'xxxxx', format: 'small', position: 4 } ]
*/
}
// Start
test()

Related

Change order of object properties based on array with indices (JavaScript)

I've been really struggling with this piece of code.
I have an object that goes like:
var obj = {
Name: 'Test',
Id: 1,
Address: 'addr'
}
And an array that goes like:
var arr = [1,0,2];
I want the properties of the object to be sorted with the indices given in the second one.
The result should then be something like:
var obj = {
Id: 1,
Name: 'Test',
Address: 'addr'
}
I'm really looking forward to your replies.
You cannot reliably set the order of the properties in an object. You will need to rethink your approach to this problem so that the order is handled by an array, not an object. The array can then be used to write code that accesses properties in a specific order, even though the object doesn't actually have them in that order.
For example:
const columns = ['Id', 'Name', 'Address'];
const data = [{
Name: 'Test',
Id: 1,
Address: 'addr'
}, {
Address: 'addr2',
Id: 1,
Name: 'Test2',
}];
let csv = columns.join(',') + '\n';
data.forEach(obj => {
const row = columns.map(propertyName => {
return obj[propertyName];
});
csv = csv + row.join(',') + '\n'
})
console.log(csv);

Merging two arrays of objects with one being used as a master overwriting any duplicates

I have two arrays of objects. Each object within that array has an array of objects.
I'm trying to merge the two arrays with one being used as a master, overwriting any duplicates in both the first level and the second 'option' level. Almost like a union join.
I've tried the code, however this doesn't cater for duplicate in options within a material.
Running this code results in two id: 400 options for the second material. When there should only be 1 with the value of 100cm.
Is there any smart way of doing this please? I also had a look at using sets, but again this only worked on the top level.
const materials_list = [
{
id: 2,
options: [
{
id: 300,
value: '50cm'
},
{
id: 400,
value: '75cm'
}
]
}
]
const master_materials_list = [
{
id: 1,
options: [
{
id: 200,
value: '50cm'
}
]
},
{
id: 2,
options: [
{
id: 400,
value: '100cm'
}
]
}
]
master_materials_list.forEach(masterMaterial => {
const matchMaterial = materials_list.find(existingMaterial => existingMaterial.id === masterMaterial.id);
if(matchMaterial) {
masterMaterial.options = masterMaterial?.options.concat(matchMaterial.options);
}
});
console.log(master_materials_list);
This is the desired output
[
{
id: 1,
options: [
{
id: 200,
value: '50cm'
}
]
},
{
id: 2,
options: [
{
id: 300,
name: '50cm'
},
{
id: 400,
name: '100cm'
}
]
}
]
Different approach that first makes a Map of the material_list options for o(1) lookup
Then when mapping the master list use filter() to find options stored in the above Map that don't already exist in the master
const materials_list=[{id:2,options:[{id:300,value:"50cm"},{id:400,value:"75cm"}]}, {id:999, options:[]}],
master_materials_list=[{id:1,options:[{id:200,value:"50cm"}]},{id:2,options:[{id:400,value:"100cm"}]}];
// store material list options array in a Map keyed by option id
const listMap = new Map(materials_list.map(o=>[o.id, o]));
// used to track ids found in master list
const masterIDs = new Set()
// map material list and return new objects to prevent mutation of original
const res = master_materials_list.map(({id, options, ...rest})=>{
// track this id
masterIDs.add(id)
// no need to search if the material list Map doesn't have this id
if(listMap.has(id)){
// Set of ids in this options array in master
const opIds = new Set(options.map(({id}) => id));
// filter others in the Map for any that don't already exist
const newOpts = listMap.get(id).options.filter(({id})=> !opIds.has(id));
// and merge them
options = [...options, ...newOpts]
}
// return the new object
return {id, options, ...rest};
});
// add material list items not found in master to results
listMap.forEach((v,k) =>{
if(!masterIDs.has(k)){
res.push({...v})
}
})
console.log(res)
.as-console-wrapper {max-height: 100%!important;top:0}
You can do this with lodash:
const materials_list = [
{
id: 2,
options: [
{
id: 300,
value: '50cm',
},
{
id: 400,
value: '75cm',
},
],
},
];
const master_materials_list = [
{
id: 1,
options: [
{
id: 200,
value: '50cm',
},
],
},
{
id: 2,
options: [
{
id: 400,
value: '100cm',
},
],
},
];
const customizer = (objValue, srcValue, propertyName) => {
if (propertyName === 'options') {
return _(srcValue)
.keyBy('id')
.mergeWith(_.keyBy(objValue, 'id'))
.values()
.value();
}
};
const merged = _(master_materials_list)
.keyBy('id')
.mergeWith(_.keyBy(materials_list, 'id'), customizer)
.values()
.value();
console.log(merged);
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.21/lodash.min.js"></script>
You’re going to have to filter matchMaterials.options before the concat. Something like:
matchMaterial.options = matchMaterial.options.filter(opt =>
masterMaterial.options.find(val => val.Id === opt.Id) == null;
);
This should remove any “duplicate” options from matchMaterial before the concat.
EDIT:
I did this on my phone so I’m sorry if the code is formatted weird like I’m seeing now

javascript how to form an array of objects by combining first Array and their values are in other arrays

Im facing a small issue in Javascript.
I have below Arrays.
var labels = ["labelOne", "labelTwo"];
var values1 = ["89", "9"];
var values2 = ["32", "78"];
Here we can place n number of values arrays like value3,value4....
Now how can i form an array of Objects by combining labels Array and their values are in values arrays. Im expecting the below output after combining above 3 arrays..
var mainArray = [{
label:"labelOne",
value:"89"
},
{
label:"labelTwo",
value:"9"
},
{
label:"labelOne",
value:"32"
},
{
label:"labelTwo",
value:"78"
}]
Can someone please help me to achieve the above output.
Thank you in advance
All that you need is a variable to know how many arrays should be added and access them in a loop using the advantage that Javascript lets you get them like this: window['variableName'] when they are defined in global scope.
var labels = ["labelOne", "labelTwo"];
var values1 = ["89", "9"];
var values2 = ["32", "78"];
var mainArray = [];
// Define a variable to know how many arrays should be added
var maxValues = 2;
function addValues(values) {
// Create new elements and push them into mainArray
mainArray.push({label:labels[0], value:values[0]});
mainArray.push({label:labels[1], value:values[1]});
}
// Do a loop from 1 to maxValues
for(let i = 1; i <= maxValues; i++) {
// Call the function with dynamic variable name
addValues(window['values' + i]);
}
console.log(mainArray);
If the order of your array isn't critical (and then, you might sort it later if it is), you can do like this:
const output = labels.map((label, index) => {
return [{ label, value: values1[index] }, { label, value: values2[index] }];
}).flat();
The map step, will give you an array like this:
[
[{ label: 'labelOne', value: 89 }, { label: 'labelOne', value: 32 }],
[{ label: 'labelTwo', value: 9}, { label: 'labelTwo', value: 78}]
]
By then calling flat, it'll transform it into:
[{ label: 'labelOne', value: 89 }, { label: 'labelOne', value: 32 }, { label: 'labelTwo', value: 9}, { label: 'labelTwo', value: 78}]
Which is what you wanted, from here you can sort the array if that matters for your use case.

Spread Array of Objects in JavaScript

I have a react-redux application, and I have a reducer named dataReducer that has a default state like this:
const defaultState = {
isLoading: false,
data: [{
id: 1,
label: 'abc',
elements: [{ color: 'red', id: 1}],
}],
};
One of the reducers adds elements to data in the defaultState. I need to test this reducer by passing the payload and then validating the new state. I want to use the spread operator to build the new state from the old defaultState, but I am having some trouble achieving it. I tried the following, but it's not working:
const newElement = {
colour: 'blue',
id: 1
};
const newState = [
{
...defaultState.data[0],
elements: [{
...defaultState.data[0].elements,
...newElement,
}]
}
];
expect(dataReducer(defaultState, action)).toEqual(newState); // returns false
It would be great if I could somehow avoid using array index (defaultState.data[0]) as there might be multiple objects in the defaultState array in the real application, though for the purpose of testing, I am keeping just one object to keep things simple.
If you're adding to the end, you spread out the other aspects of state in the new state object, then override data with the current contents of it followed by the new entry:
const newState = { // New state is an object, not an aray
...defaultState, // Get everything from defaultState
data: [ // Replace `data` array with a new array
{
...defaultState.data[0], // with the first item's contents
elements: [ // updating its `elements` array
...defaultState.data[0].elements,
newElement
]
},
...defaultState.data.slice(1) // include any after the first (none in your example)
]
};
Live Example:
const defaultState = {
isLoading: false,
data: [{
id: 1,
label: 'abc',
elements: [{ color: 'red', id: 1}],
}],
};
const newElement = {
colour: 'blue',
id: 1
};
const newState = { // New state is an object, not an aray
...defaultState, // Get everything from defaultState
data: [ // Replace `data` array with a new array
{
...defaultState.data[0], // with the first item's contents
elements: [ // updating its `elements` array
...defaultState.data[0].elements,
newElement
]
},
...defaultState.data.slice(1) // include any after the first (none in your example)
]
};
console.log(newState);
.as-console-wrapper {
max-height: 100% !important;
}
There's no getting around specifying the entry in data that you want (e.g., data[0]).
In a comment you've asked how to handle this:
Let's say data (present inside defaultState) has multiple objects entries in it. First object has id of 1, second one has id of 2. Now the newElement to be added has an id of 2. So the newElement should get added to the second object. Where in second object? Inside the elements property of the second object. The addition should not over-write existing entries in the elements array.
You'll need to find the index of the entry in data:
const index = defaultState.data.findIndex(({id}) => id === newElement.id);
I'm going to assume you know that will always find something (so it won't return -1). To then apply that index to the code above, you'd do this:
const newState = { // New state is an object, not an aray
...defaultState, // Get everything from defaultState
data: [ // Replace `data` array with a new array
...defaultState.data.slice(0, index), // Include all entries prior to the one we're modifying
{
...defaultState.data[index], // Include the entry we're modifying...
elements: [ // ...updating its `elements` array
...defaultState.data[index].elements,
newElement
]
},
...defaultState.data.slice(index + 1) // include any after the one we're updating
]
};
The only real change there is adding the ...defaultState.data.slice(0, index) at the beginning of the new data, and using index instead of 0.
Live Example:
const defaultState = {
isLoading: false,
data: [
{
id: 1,
label: 'abc',
elements: [{ color: 'red', id: 1}],
},
{
id: 2,
label: 'def',
elements: [{ color: 'green', id: 2}],
},
{
id: 3,
label: 'ghi',
elements: [{ color: 'yellow', id: 3}],
}
],
};
const newElement = {
colour: 'blue',
id: 2
};
const index = defaultState.data.findIndex(({id}) => id === newElement.id);
const newState = { // New state is an object, not an aray
...defaultState, // Get everything from defaultState
data: [ // Replace `data` array with a new array
...defaultState.data.slice(0, index), // Include all entries prior to the one we're modifying
{
...defaultState.data[index], // Include the entry we're modifying...
elements: [ // ...updating its `elements` array
...defaultState.data[index].elements,
newElement
]
},
...defaultState.data.slice(index + 1) // include any after the one we're updating
]
};
console.log(newState);
.as-console-wrapper {
max-height: 100% !important;
}

Concatenate Nested Array after Mapping

I have an array with several category objects, each of which has an items property containing an array of item objects. I want to map each item in each category to an object[] with objects that have the properties value and label. For some reason, I can't perform the concatenation.
var categories = [{
name: "category1",
items: [{
itemId: 1,
name: "Item1"
}, {
itemId: 2,
name: "Item2"
}]
}, {
name: "category2",
items: [{
itemId: 3,
name: "Item3"
}, {
itemId: 4,
name: "Item4"
}]
}];
var items = [];
for(var i = 0; i < categories.length; i++){
items.concat($.map(categories[i].items,function(elem){
return {value:elem.itemId, label:elem.name};
}));
}
console.log(items); //prints []
Expected Result
[{
label: "Item1",
value: "1"
},
{
label: "Item2",
value: "2"
},{
label: "Item3",
value: "3"
},{
label: "Item4",
value: "4"
}
I feel as if I am missing something very basic. I logged the result of the $.map function and it appears to be returning an []. Can anyone figure out the issue?
JSFiddle: http://jsfiddle.net/vymJv/
Another method using straight Javascript:
var x = categories.map(function(val) {
return val.items;
}).reduce(function(pre, cur) {
return pre.concat(cur);
}).map(function(e,i) {
return {label:e.name,value:e.itemId};
});
Output: x = [{label: "Item1", value: 1}, {label: "Item2", value: 2}, …]
The concat() method is used to join two or more arrays.
This method does not change the existing arrays, but returns a new
array, containing the values of the joined arrays.
http://jsfiddle.net/vymJv/1/
for(var i = 0; i < categories.length; i++){
items = items.concat($.map(categories[i].items, function(elem) {
return {value: elem.itemId, label: elem.name};
}));
}
updated with flatMap(not compatible with IE)
categories.flatMap((categories) => categories.items)
flatMap() method returns a new array formed by applying a given callback function to each element of the array, and then flattening the result by one level.
const items = categories
.map(category => category.items)
.reduce((prev, current) => [...prev, ...current])
.map((e, i) => ({ label: e.name, value: e.itemId }));
We could extend the array prototype by creating concatAll which will concatenate all of your arrays by iterating over each subarray, dumping each value into a new array.
Array.prototype.concatAll = function() {
var results = [];
this.forEach(function(subArray) {
subArray.forEach(function(subArrayValue) {
results.push(subArrayValue);
});
});
return results;
};
Then, we can get the desired result with the following:
let items = categories.map(function(category) {
return category.items.map(function(item) {
return {label: item.name, value: item.itemId};
});
}).concatAll();
We get the items by translating each category into an array of items. Because we have two categories, we will end up with two arrays of items. By applying concatAll on the final result, we flatten those two arrays of items and get the desired output.
This piece of code solves your task using a functional programming approach:
var items = [].concat.apply([], categories.map(cat =>
cat.items.map(elem => ({ value:elem.itemId, label:elem.name })))
)
Explanation: Function.prototype.apply() has the syntax fun.apply(thisArg, [argsArray]) and lets us provide parameters to a function in an array. Array.prototype.concat() combines an arbitrary amount of parameters into one array. If we now write Array.prototype.concat.apply([], [category1Items, ..., categoryNItems]), it actually equals [].concat(category1Items, ..., categoryNItems), which concatenates all parameters together. You can also replace Array.prototype.concat by [].concat to keep it shorter. Otherwise we just use standard mapping to get the job done.
You could also split the code apart a bit more for clarity:
function getItem(elem){
return {value:elem.itemId, label:elem.name};
}
function getCategoryItems(cat) {
return cat.items.map(getItem);
}
function flatten(arr) {
return Array.prototype.concat.apply([], arr);
}
var items = flatten(categories.map(getCategoryItems));

Categories

Resources