JavaScript how to build array from nested Object using recursion without mutation - javascript

interface SubscriptionParams {
selectedProduct?: SubscriptionSelectedProduct;
}
interface SubscriptionSelectedProduct {
productId?: string;
pricingId?: string;
childProduct?: SubscriptionSelectedProduct;
}
function getChildIdRecursively(product: SubscriptionSelectedProduct, ids: string[]) {
if (product) {
ids.push(product.productId!);
product.childProduct && getChildIdRecursively(product.childProduct, ids);
}
}
function subscriptionProductsIds(subscription: SubscriptionParams): string[] {
let ids: string[] = [subscription.selectedProduct?.productId!];
if (subscription.selectedProduct?.childProduct) {
getChildIdRecursively(subscription.selectedProduct?.childProduct, ids);
}
return ids;
}
How to make this recursion without mutation, now I am mutating ids array.
I want to follow functional programming principles

I think you could resolve this by leveraging tree traversals IMHO.
const collectIds = (node) => {
const { id, children = [] } = node;
// const children = [product.childProduct] in your specific case
const ids = children.flatMap(collectIds);
return [id, ...ids];
};
const product = {
id: 'L1',
children: [
{ id: 'L1.1' },
{
id: 'L1.2',
children: [
{ id: 'L1.2.1', },
{ id: 'L1.2.2', },
{ id: 'L1.2.3', },
{ id: 'L1.2.4', },
{ id: 'L1.2.5', },
],
},
],
};
console.log(
collectIds(product),
);

I'd avoid creating a lot of unnecessary intermediate arrays, but to each their own. Making this "immutable" is as easy as returning a new array from the getChildIdRecursively. Also since you are basically duplicating the logic in subscriptionProductsIds you can remove that.
function getChildIdRecursively(product: SubscriptionSelectedProduct) {
if (product) {
let ids: string[] = [product.productId!];
if (product.childProduct) {
ids = ids.concat(getChildIdRecursively(product.childProduct));
}
return ids;
}
return [];
}
function subscriptionProductsIds(subscription: SubscriptionParams): string[] {
return getChildIdRecursively(subscription.selectedProduct)
}

Related

JavaScript array.map always returning the last item

I have an array of objects, objects look like this:
interface IDog{
id: number;
name: string;
summary: string;
wikiLink: string;
imageLink: string;
imageAlt: string;
}
I have this function:
private processDog(dogs: IDog[]) {
return dogs.map((dog) => {
let displayCard = this.dogDisplay;
find(displayCard.body, { id: "cardHeader" }).items[0].text = dog.name;
let adaptiveCard = CardFactory.adaptiveCard(displayCard);
let preview = this.dogPreview;
preview.content.title = dog.name;
return { ...adaptiveCard, preview };
});
}
Now when I call this function, passing an array of Dogs, let's say [DogA, DogB, DogC, DogD]
let att = this.processDog(dogs);
I am expecting to return an array of the processed dogs as needed, however what's returned is an array of 4 processed dogs of type DogD.. meaning that DogD is shadowing all other dogs in the returned array. What am I doing wrong?
Full code:
//First method
public async onQuery(
context: TurnContext,
query: MessagingExtensionQuery
): Promise<MessagingExtensionResult> {
const dogs: any = require("./dogs.json");
let att = this.processdog(dogs);
console.log(att);
return Promise.resolve({
type: "result",
attachmentLayout: "list",
attachments: att, // [{ ...adaptiveCard, preview: defaultPreview }],
} as MessagingExtensionResult);
}
//second method
private processdog(dogs: Idog[]) {
return dogs.map((dog) => {
let displayCard = Object.assign({}, this.dogDisplayCard);
find(displayCard.body, { id: "cardHeader" }).items[0].text = dog.name;
find(displayCard.body, { id: "cardBody" }).items[0].text = dog.summary;
find(displayCard.body, {
id: "cardBody",
}).items[1].columns[0].items[0].url = dog.imageLink;
find(displayCard.body, { id: "cardBody" }).items[2].text = dog.imageAlt;
displayCard.actions[0].url = dog.wikiLink;
let adaptiveCard = CardFactory.adaptiveCard(displayCard);
let preview = Object.assign({}, this.dogPreviewCard);
preview.content.title = dog.name;
preview.content.text = dog.imageAlt;
preview.content.images[0].url = dog.imageLink;
return { ...adaptiveCard, preview };
});
}
dogPreview.json:
{
"contentType": "application/vnd.microsoft.card.thumbnail",
"content": {
"title": "",
"text": "",
"images": [
{
"url": ""
}
]
}
}
You need to create a deep copy of your objects like so
...
let displayCard = JSON.parse(JSON.stringify(this.dogDisplayCard));
...
let preview = JSON.parse(JSON.stringify(this.dogPreviewCard));
...
Otherwise, you will be mutating the original object with the lines that follow. { ...adaptiveCard, preview } will contain references to the original object, so will update when the original object does. So, you must ensure that you are working with a fresh copy of the object that does not have any references to the original object.

Transform all child objects using recursive reduce in ES6

I'm trying to create a set of reducers in order to change an attribute of all objects in a nested list.
The input payload looks like the following:
const payload = [
{
name: "Peter",
children: [
{
name: "Sarah",
children: [
{
name: "Sophie",
children: [
{
name: "Chris"
}
]
}
]
}
]
}
];
I now want to change the name attribute of all elements and child elements.
const mapJustNickname = elem => {
return {
...elem,
nickname: elem.name + "y"
};
};
How do I use this map function recursively on all child elements?
I found a way to do this by putting the the recursion within the same mapping function.
const mapToNickname = (elem) => {
return {
nickname: elem.name +'y',
children: elem.children && elem.children.map(mapToNickname)
}
}
console.log(payload.map(mapToNickname));
But I'd like to have the mapping of the name separated from the recursion (for reasons of keeping the mapping functions as simple as possible) and being able to chain them later. Is it somehow possible to do this with two reducers and then chaining them together?
Let's start by rigorously defining the data structures:
data Person = Person { name :: String, nickname :: Maybe String }
data Tree a = Tree { value :: a, children :: Forest a }
type Forest a = [Tree a]
type FamilyTree = Tree Person
type FamilyForest = Forest Person
Now, we can create mapTree and mapForest functions:
const mapTree = (mapping, { children=[], ...value }) => ({
...mapping(value),
children: mapForest(mapping, children)
});
const mapForest = (mapping, forest) => forest.map(tree => mapTree(mapping, tree));
// Usage:
const payload = [
{
name: "Peter",
children: [
{
name: "Sarah",
children: [
{
name: "Sophie",
children: [
{
name: "Chris"
}
]
}
]
}
]
}
];
const mapping = ({ name }) => ({ name, nickname: name + "y" });
const result = mapForest(mapping, payload);
console.log(result);
Hope that helps.
Create a recursive map function that maps an item, and it's children (if exists). Now you can supply the recursiveMap with a ever transformer function you want, and the transformer doesn't need to handle the recursive nature of the tree.
const recursiveMap = childrenKey => transformer => arr => {
const inner = (arr = []) =>
arr.map(({ [childrenKey]: children, ...rest }) => ({
...transformer(rest),
...children && { [childrenKey]: inner(children) }
}));
return inner(arr);
};
const mapNickname = recursiveMap('children')(({ name, ...rest }) => ({
name,
nickname: `${name}y`,
...rest
}));
const payload = [{"name":"Peter","children":[{"name":"Sarah","children":[{"name":"Sophie","children":[{"name":"Chris"}]}]}]}];
const result = mapNickname(payload);
console.log(result)

Add item into array instead of overriding the current value with the new value

I'm trying to figure out how to add new items into array instead of overriding the current value with the new value. I'm using .push() which should add the item every time it maps through the array. Any Ideas?
const searchChips = [
{value: "string"}, {value: "test"}
];
const query = {
bool: {
filter: []
}
};
const searchQuery = {
query_string: {
query: ""
}
};
searchChips.map(chip => {
console.log(chip);
const key = "query";
searchQuery.query_string[key] = chip.value;
query.bool.filter.push(searchQuery);
});
console.log(query);
You are inserting the same query since you are dealing with the same exact reference to the searchQuery. Instead of this try having it as a function which returns an object:
const searchChips = [{
value: "string"
}, {
value: "test"
}];
const query = {
bool: {
filter: []
}
};
let sq = (query) => ({
query_string: {query}
});
searchChips.map(chip => query.bool.filter.push(sq(chip.value)));
console.log(query);
This will return to you the 2 filters each with different values for query_string since now the function will return an entirely new object instead of you dealing with the same reference.
The problem seems to be that you are pushing into query.bool.filter outside the .map() function. Try this.
const searchChips = [{ value: "string" }, { value: "test" }];
const query = {
bool: {
filter: []
}
};
searchChips.forEach(chip => {
const key = "query";
const searchQuery = {
query_string: {
query: ""
}
};
searchQuery.query_string[key] = chip.value;
query.bool.filter.push(searchQuery);
});
console.log(JSON.stringify(query));

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.

Javascript Recursion Tree Building

I am having an issue building a tree from a flat array. I am building a category -> subcategory tree in which the parent has subcategories as an array.
Here is what the flat array would look like:
[
{
"id": 1
},
{
"id": 5,
},
{
"id": 2,
"parent_id": 1
},
{
"id": 3,
"parent_id": 1
},
{
"id": 42,
"parent_id": 5
},
{
"id": 67,
"parent_id": 5
}
]
And this is what I need the result to look:
[
{
"id":1,
"subcategories":[
{
"id":2,
"parent_id":1
},
{
"id":3,
"parent_id":1
}
]
},
{
"id":5,
"subcategories":[
{
"id":42,
"parent_id":5
},
{
"id":67,
"parent_id":5
}
]
}
]
I have tried to do this recursively by recursively searching for children and attaching it as an array and continuing to do so until I hit the bottom of the barrel but I am getting a cyclic structure. It appears that the parent_id in traverse is always the id of the parent... any ideas:
tree(passingInFlatObjectHere);
function topLevel (data) {
let blob = [];
data.forEach((each) => {
if (!each.parent_id) {
blob.push(each);
}
});
return blob;
}
function tree (data) {
let blob = topLevel(data).map(function (each) {
each.subcategories = traverse(data, each.id);
return each;
});
return blob;
}
function traverse (data, parent_id) {
let blob = [];
if (!parent_id) {
return blob;
}
data.forEach((each) => {
if (each.id === parent_id) {
each.subcategories = traverse(data, each.id);
blob.push(each);
}
});
return blob;
}
I don’t just want to help you fix your problem but would also like to help you take full advantage of ES6
First of all your topLevel function can be rewritten to this:
function topLevel(data) {
return data.filter(node => !node.parent_id);
}
Neat isn’t it? I also would recommend slightly changing tree for consistency but that’s of course just stylistic.
function tree(data) {
return topLevel(data).map(each => {
each.subcategories = traverse(data, each.id);
return each;
});
}
No logic issues so far. traverse, however, contains one, when you check each.id === parent_id. Like this, the functions searches for the node whose id is parent_id. Obviously a mistake. You wanted each.parent_id === parent_id.
Your issue is solved now. Stop reading if I bother you. But you could also take advantage of filter here and remove that slightly superfluous early exit and rewrite your function to:
function traverse(data, parentId) {
const children = data.filter(each => each.parent_id === parentId);
children.forEach(child => {
child.subcategories = traverse(data, child.id);
});
return children;
}
First, you need install lodash with below command with npm:
npm i lodash
Second, you must import _ from lodash
import _ from "lodash";
Finally, run this function:
export const recursive_lists = (data) => {
const grouped = _.groupBy(data, (item) => item. parent_id);
function childrenOf(parent_id) {
return (grouped[parent_id] || []).map((item) => ({
id: item.id,
child: childrenOf(item.id),
}));
}
return childrenOf(null);
};
or
First:
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.20/lodash.min.js"></script>
Second:
function recursive_lists(data) {
const grouped = _.groupBy(data, (item) => item.parent_id);
function childrenOf(parent_id) {
return (grouped[parent_id] || []).map((item) => ({
id: item.id,
child: childrenOf(item.id),
}));
}
return childrenOf(null);
};

Categories