Keep original key when re-looping through nested object - javascript

I've got a nested object with child objects and arrays. Something like this:
const documents = {
invoice: {
documentID: '_e4564',
displayName: '2019-02-03',
url: 'https://www.urltoinvoice.com'
},
conditions: {
documentID: '_e9365',
displayName: 'Conditions company x',
url: 'https://www.urltoconditions.com'
},
reminders: [
{
documentID: '_e4364',
displayName: 'First reminder',
url: 'https://www.urltofirstreminder.com'
},
{
documentID: '_e0254',
displayName: 'Second reminder',
url: 'https://www.urltosecondreminder.com'
},
]
}
I'm trying to create a new array of objects to use in a select box.
The child objects need the same properties but with an updated displayName based on the document type. So, for example, reminder: First reminder .
Currently, this is my code:
const newArray = [];
this.addDocumentToArray(documents, newArray);
and the addDocumentToArray function:
addDocumentToArray = (documents, arr) => {
Object.entries(documents).forEach(([key, val]) => {
if (Array.isArray(val)) {
this.addDocumentToArray(val, arr);
} else {
arr.push({ documentID: val.documentID, displayName: `${key}: ${val.displayName}` });
}
});
}
The output at this point is an array that looks like this:
0: {documentID: "_e4564", displayName: "invoice: 2019-02-03"}
1: {documentID: "_e9365", displayName: "conditions: Conditions company x"}
2: {documentID: "_e4364", displayName: "0: First reminder"}
3: {documentID: "_e0254", displayName: "1: Second reminder"}
Almost ok but the key of the reminders is 0 and 1. How can I get reminder (or reminders) as key?

You can add third optional parameter to function labelKey. You are passing that parameter only if your value is array and it will use that value as key in else part
addDocumentToArray = (documents, arr, labelKey) => {
Object.entries(documents).forEach(([key, val]) => {
if (Array.isArray(val)) {
this.addDocumentToArray(val, arr, key);
} else {
arr.push({ documentID: val.documentID, displayName: `${labelKey || key}: ${val.displayName}` });
}
});
}

I really like chriss' answer, so I'll try to write an alternative:
let currentKey = null;
addDocumentToArray = (documents, arr) => {
Object.entries(documents).forEach(([key, val]) => {
if (Array.isArray(val)) {
let prevKey = currentKey;
currentKey = key;
this.addDocumentToArray(val, arr);
currentKey = prevKey;
} else {
arr.push({ documentID: val.documentID, displayName: `${currentKey || key}: ${val.displayName}` });
}
});
}

Related

Assure all ids are unique with RxJS Observable pipe

I have an observable that I'd like to modify before it resolves, either using a map pipe or something similar to ensure that all ids within the groups array are unique. If cats is encountered twice, the second occurrence should become cats-1, cats-2 etc. These fields are being used to populate a HTML id attribute so I need to ensure they are always unique.
{
title: 'MyTitle',
description: 'MyDescription',
groups: [
{
id: 'cats',
title: 'SomeTitle'
},
{
id: 'dogs',
title: 'SomeTitle'
},
{
id: 'octupus',
title: 'SomeTitle'
},
{
id: 'cats',
title: 'SomeTitle'
},
]
}
Using an RxJs observable my code looks like the following:
getGroups() {
return this.http.get(ENDPOINT_URL)
}
I was able to achieve this using a map operator with a set but part of me feels like this isn't the correct pipe for this as the array is nested.
getGroups() {
return this.http.get(ENDPOINT_URL).pipe(
map(data => {
const groupIds = new Map();
data.groups.map(group => {
if (!groupIds.get(group.id)) {
groupIds.set(group.id, 1)
} else {
const updatedId = (groupIds.get(group.id) || 0) + 1;
groupIds.set(group.id, updatedId);
group.id = `${group.id}-${updatedId}`
}
return group
}
return data;
}
)
}
Is there a more efficient way to make this operation using a more appropriate pipe? I am worried this can become quite inefficient and significantly delay rendering of content while the observable resolves the conflicts. As of today I am unable to modify the actual content returned from the API so that is not an option unfortunately.
You could try something like this:
import { of, map } from 'rxjs';
import { findLastIndex } from 'lodash';
of({
title: 'MyTitle',
description: 'MyDescription',
groups: [
{
id: 'cats',
title: 'SomeTitle',
},
{
id: 'dogs',
title: 'SomeTitle',
},
{
id: 'cats',
title: 'SomeTitle',
},
{
id: 'octupus',
title: 'SomeTitle',
},
{
id: 'cats',
title: 'SomeTitle',
},
],
})
.pipe(
map((data) => ({
...data,
groups: data.groups.reduce((acc, group) => {
const lastElementIndex = findLastIndex(acc, (accGroup) => accGroup.id.startsWith(group.id));
if (lastElementIndex === -1) {
return [...acc, group];
}
const lastElement = acc[lastElementIndex];
const lastNameNumerator = lastElement.id.split('-')[1];
return [
...acc,
{
...group,
id: `${group.id}-${lastNameNumerator ? +lastNameNumerator + 1 : 1}`,
},
];
}, []),
}))
)
.subscribe(console.log);
Stackblitz: https://stackblitz.com/edit/rxjs-kcxdcw?file=index.ts
If the only requirement is to have the ids be unique, you could ensure uniqueness by appending the array index to each element's id.
getGroups() {
return this.http.get(ENDPOINT_URL).pipe(
map(data => {
const groups = data.groups.map(
(g, i) => ({...g, id: `${g.id}-${i}`})
);
return { ...data, groups };
})
);
}
Output of groups:
// groups: Array[5]
// 0: Object
// id : "cats-0"
// title : "SomeTitle"
//
// 1: Object
// id : "dogs-1"
// title : "SomeTitle"
//
// 2: Object
// id : "cats-2"
// title : "SomeTitle"
//
// 3: Object
// id : "octupus-3"
// title : "SomeTitle"
//
// 4: Object
// id : "cats-4"
// title : "SomeTitle"
Here's a little StackBlitz.
Honestly what you have is probably fine. Here's another method that's slightly simpler. It first uses reduce to create an object literal of groups. If you were open to external dependencies you could use Ramda's groupWith function to produce the same result. Then it uses flatMap to flatten the groups. If there is only one item in the array then it is returned as is, otherwise the elements are mutated with the new ids.
getGroups() {
return this.http.get(ENDPOINT_URL).pipe(
map(data => Object.values(
data.groups.reduce((acc, cur) => {
(acc[cur.id] || (acc[cur.id] = [])).push(cur);
return acc;
},
{} as Record<string | number, [] as GroupType[])
).flatMap(grp => (grp.length === 1)
? grp
: grp.map((x, i) => ({ ...x, id: `${x.id}-${i + 1}`)))
)
}
Another one
map((data:any) => {
//create an array in the way [{id:"cats",data:[0,3]}{id:"dogs",data:[1]..]
const keys=data.groups.reduce((a:any,b:any,i:number)=>{
const el=a.find(x=>x.id==b.id)
if (el)
el.data=[...el.data,i]
else
a=[...a,({id:b.id,data:[i]})]
return a
},[])
//loop over groups, if keys.data.length>1 ...
data.groups.forEach((x,i)=>{
const el=keys.find(key=>key.id==x.id)
if (el.data.length>1)
x.id=x.id+'-'+(el.data.findIndex(l=>l==i)+1)
})
return data;
})
Or
map((data:any) => {
//create an object keys {cats:[0,3],dogs:[1]....
const keys=data.groups.reduce((a:any,b:any,i:number)=>{
if (a[b.id])
a[b.id]=[...a[b.id],i]
else
a[b.id]=[i]
return a
},{})
//loop over groups, if keys[id].length>0 ...
data.groups.forEach((x,i)=>{
if (keys[x.id].length>1)
x.id=x.id+'-'+(keys[x.id].findIndex(l=>l==i)+1)
})
return data;
})

The best way to filter down a dataSet with javascript

The idea was to query a dataset with querystring params. I only want the "records" to match only what was queried.
Dataset
{
1111:
{
Category: "Education"
Role: "Analyst"
}
2222:
{
Category: "Communications and Media"
Role: "Analyst"
}
3333:
{
Category: "Public Sector"
Role: "Something else"
}
4444:
{
Category: "Public Sector"
Role: "Something else"
}
...
}
[[Prototype]]: Object
I'm sending in qString
Category: (2) ['Communications and Media', 'Education']
Role: ['Analyst']
length: 0
[[Prototype]]: Array(0)
I'd like to loop over that and filter/reduce so I only have records that match. Sort of an and instead of an or.
dataSet is an Object of objects. Thoughts? Thanks in advance.
export const Filtered = (qStrings, dataSet) => {
const filtered = [];
Object.entries(qStrings).forEach(([field]) => {
qStrings[field].forEach((value) => {
filtered.push(
..._.filter(dataSet, (sess) => {
if (sess[field] && sess[field].toString() === value.toString()) {
return sess;
}
})
);
});
});
return _.uniq(filtered);
};
geez, I figured it out with a colleague who's way smarter than me wink Jess!
export const Filtered = (qStrings, dataSet) => {
let filtered = [];
Object.entries(qStrings).forEach(([field], idx) => {
let source = filtered;
if (idx === 0) {
source = dataSet;
}
filtered = _.filter(source, (sess) => {
return sess[field] && sess[field].includes(qStrings[field]);
});
});
return _.uniq(filtered);
};
Now to clean this up.
Not sure if this solves your problem exactly, but you can apply this logic without mutation for a much cleaner function.
export const matches = (qStrings, dataSet) =>
Object.entries(dataSet).reduce((acc, [key, value]) =>
Object.entries(value).every(([rKey, rValue]) => qStrings[rKey]?.includes(rValue))
? { ...acc, [key]: value }
: acc,
{});
This will return records 1111 and 2222 because they match one of the categories and the role in qStrings.

Object name from array value

I have array with some values :
let typeArray = ["name", "strret", "car", "type"];
And I have one object :
let formDefinitionObject = {schema:{}}
I want the formDefinitionObject object be like :
let formDefinitionObject = {
schema: {
name: {
type: 'string',
title: 'Name',
required: true
},
strret: {
type: 'string',
title: 'Strret'
},
car: {
type: 'string',
title: 'Car'
},
type: {
type: 'string',
title: 'Type'
}
}
}
I want dynamically for each item in array to be object in formDefinitionObject.schema object. For example if i add one more item in array typeArray.push('country') to automatic add this object in formDefinitionObject.schema object.
Couldn't understand how required: true would fit in. Remaining things can be done as follows
var getFormDefinition = function(arr) {
function getSchemaObj(arr) {
return arr.map(d => ({
[d]: {
type: typeof(d),
title: d
}
}))
.reduce((a, b) => ({ ...a, ...b }))
}
var schemaObj = getSchemaObj(arr)
arr.push = function(...d) {
Object.assign(schemaObj, getSchemaObj(d))
return Array.prototype.push.call(arr, ...d)
}
return ({
schema: schemaObj
})
}
var typeArray = ["name", "strret", "car", "type"];
var result = getFormDefinition(typeArray)
console.log(result)
typeArray.push('nitish')
console.log(result)
Even though you did not clarify more how a field has to be required or does any field has to be as string, here's a solution based on what you provided so far.
The next snippet does the job and it has some explanations :
let typeArray = ['name', 'strret', 'car', 'type'],
formDefinitionObject = {
schema: {}
};
/** cycle through "typeArray" and populate "formDefinitionObject.schema" **/
typeArray.forEach(el => {
let currObj = {
type: 'string', /** YOU DID NOT SPECIY HOW TO DECIDE THE TYPE **/
title: el[0].toUpperCase() + el.substring(1), /** the title with the first letter being capitalized as you provided in the question. You can just use "el" instead of "el[0].toUpperCase() + el.substring(1)" if you'd like to print as it is **/
};
el === 'name' && (currObj['required'] = true); /** YOU DID NOT SPECIY HOW TO DECIDE IF A FIELD HAS TO BE REQUIRED. I just saw only the "name" as required so I did a basic (yet a stupid) check if the current element is "name" add a required to it **/
formDefinitionObject.schema[el] = currObj; /** add the current record to the "schema" attribute **/
});
console.dir(formDefinitionObject); /** printing the result **/
I'll be here if you answer our questions in the comments section.
Til then, hope I pushed you further.
You could use Proxy on the typeArray with the set trap so each time you push new value to the proxy array you can also add new property to your schema. This way you can simulate observer pattern.
You can also create some pattern to add additional properties like required for example name:prop1:prop2 but this way value is fixed to true.
let typeArray = ["name:required", "strret", "car", "type"];
let formDefinitionObject = {
schema: {}
}
let proxyArray = new Proxy(typeArray, {
set(obj, prop, value) {
if (prop != 'length') addToSchema(formDefinitionObject.schema, value);
return Reflect.set(...arguments);
}
})
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function addToSchema(schema, prop) {
const [name, ...params] = prop.split(':');
schema[name] = {
type: 'string',
title: capitalize(name)
}
params.forEach(param => schema[name][param] = true);
return schema;
}
proxyArray.reduce(addToSchema, formDefinitionObject.schema);
proxyArray.push('email:required:isEmail');
proxyArray.push('phone');
console.log(formDefinitionObject)
Update: You could use something like this name:prop1|value:prop2 to add property value other then true but if you don't specify value default is still true
let typeArray = ["name:required", "strret", "car", "type"];
let formDefinitionObject = {
schema: {}
}
let proxyArray = new Proxy(typeArray, {
set(obj, prop, value) {
if (prop != 'length') addToSchema(formDefinitionObject.schema, value);
return Reflect.set(...arguments);
}
})
function capitalize(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
function addToSchema(schema, prop) {
const [name, ...params] = prop.split(':');
schema[name] = {
type: 'string',
title: capitalize(name)
}
params.forEach(param => {
const [key, value] = param.split('|');
schema[name][key] = value ? value : true
});
return schema;
}
proxyArray.reduce(addToSchema, formDefinitionObject.schema);
proxyArray.push('email:required:isEmail');
proxyArray.push('phone:default|123/555-333:required');
proxyArray.push('address')
console.log(formDefinitionObject)

Updating state as a nested object

I have this type of state in my app
state = {
abc: true,
array: [
{ id: 12345, done: false },
{ id: 10203, done: false },
{ id: 54321, done: false }
]
};
I am looking for a solution to the following problem: I need to change done property accordingly to passed id like in the following function when something like this handle(12345) is passed as an argument to handle function :
handle = id => {
this.state.array.map(e => {
if (e.key === id) {
this.setState({array: [
{ id: id, done: true },
{ id: 10203, done: false },
{ id: 54321, done: false }
]})
}
});
};
In simple words I need to change just one object in array based on provided id.
Thanks for any help or tips!
I'd write the handle method as:
handle = id => {
this.setState(prevState => {
const { array } = prevState;
return {
array: [
...array.filter(o => o.id !== id),
{id, done: true}
]
};
});
};
The idea here is that, remove the matching id from old state, and then add a new object to the array with id and done property as {id, done: true}.
Once you are allowed to restructure state to be hashmap instead of array:
state = {
abc: true,
array: {
12345: { done: false },
10203: { done: false },
54321: { done: false }
]
};
then you will be able to use power of spread operator:
let id = 12345;
this.setState({
array: {
...this.state.array,
[id]: {
...this.state.array[id],
done: true
}
}
});
Otherwise using array.map() seems to be the only option
You can use this Redux pattern to return a new array with the element in question being changed, while keeping your array immutable:
handle = id => {
const updatedArray = this.state.array.map(e => {
if (e.key === id) {
return { id: id, done: true };
}
else {
return e;
}
});
this.setState({array: updatedArray});
};
This keeps your data structures immutable while changing only what you need to change.
var newArray = this.state.array;
for(var i = 0; i < newArray.length; i++){
if(newArray[i].id === 12345) {
newArray[i].done = true;
}
}
this.setState({array: newArray});
By creating the newArray here you will be avoiding directly touching the state element, so you can change anything you want inside it afterwards you can set the state.

pick and omit in one function (lodash) in Redux action object

Basically what I need is to get "items" from json and re-assign it, and leave other keys untouched:
The first approach duplicates data.
The second seems to be bad in terms of performance.
And the third is hard to read and understand.
I use lodash. But if it can't be done in a clever fashion, you can suggest me a different library.
function a(name, json) {
return {
type: RECEIVE_DATA,
name,
items: _.get(json, 'data.items', []),
receivedAt: Date.now(),
...json,
};
}
function b(name, json) {
return {
..._.omit(json, 'data'),
type: RECEIVE_DATA,
name,
items: _.get(json, 'data.items', []),
receivedAt: Date.now(),
}
}
function c(name, json) {
return {
..._.transform(json, (result, value, key) =>{
if (key === 'data') {
result['items'] = value['items'];
} else {
result[key] = value;
}
}, {}),
type: RECEIVE_DATA,
name,
receivedAt: Date.now(),
}
}
Use parameters destructuring to get items, and rest properties to collect the rest of the params:
const json = { another: [], data: { items: [1] } };
function a(name, { data, data: { items = [] }, ...jsonRest } = {}) {
return {
type: 'RECEIVE_DATA',
name,
items,
receivedAt: Date.now(),
...jsonRest
};
}
console.log(a('name', json));
You can also shorten the action creator a bit by using an arrow function:
const a = (name, { data, data: { items = [] }, ...jsonRest } = {}) => ({
type: RECEIVE_DATA,
name,
items,
receivedAt: Date.now(),
...jsonRest
});
If you don't mind having a couple more lines in your function, you could easily just grab the data from that key then delete it:
function a(name, json = {}) {
const { items = [] } = json.data;
delete json.data;
return {
type: RECEIVE_DATA,
name,
items,
receivedAt: Date.now(),
...json,
};
}

Categories