The goal of this is to have comments with recursive child comments.
As example data for this, there are posts and comments that got fetched.
let posts = [
{ id: '001a', topic: 'post topic', content: 'post content' }
]
let comments = [
{ id: '002a', postParent: '001a', directParent: '001a', content: 'comment on post' },
{ id: '003a', postParent: '001a', directParent: '002a', content: 'comment on comment' },
{ id: '004a', postParent: '001a', directParent: '003a', content: 'comment on comments comment' },
]
If I should formulate it into words, I would say: conditionally on a comment-objects parent-key, this object should become a child-comment-object of its parent-comment-object. To achieve this, I came to think that it needs a functionality to create another array. Like:
let postComments = [
{
id: '002a',
postParent: '001a',
directParent: '001a',
content: 'comment on post',
children: [
{
id: '003a',
postParent: '001a',
directParent: '002a',
content: 'comment on comment',
children: [ {id: '004a', post: '001a', parent: '003a', content: 'comment on comments comment' }} ]
}
]
},
]
The approach I tried until now, was solving this without creating such a new array that extends itself. I tried outputting the comments conditionally on it's parent in an each-block. Unfortunately this didn't leadt to a scalable solution (/ to do it recursively -I'm not sure about the right terminology here). That's how I got to the approach this question is now about and how to create a new array like shown in the example.
But as I'm still not sure if it's a good solution when having such data, any suggestion on a proven approach for such a problem is very welcome.
Unfortunately the extensive REPL I was thankfully able to create from the provided answers was overwritten when viewing and saving another REPL. I had to delete the now misleading link, but the little takeaway is to make a backup of svelte REPLS if some work went into it. Which I didn't. Anyway the answers below hold enough good examples.
let posts = [{ id: '001a', topic: 'post topic', content: 'post content' }]
let comments = [
{ id: '002a', post: '001a', parent: '001a', content: 'comment on post' },
{ id: '003a', post: '001a', parent: '002a', content: 'comment on comment' },
{ id: '004a', post: '001a', parent: '003a', content: 'comment on comments comment' },
]
function getNestedComments(parentId) {
const subComments = comments.filter((c) => c.parent === parentId)
if (subComments.length === 0) return subComments
subComments.forEach(c => {
c.children = getNestedComments(c.id)
})
return subComments
}
let postComments = getNestedComments('001a')
console.log(postComments)
Here is another solution with no recursive function. It uses map-lookup.
let posts = [
{ id: '001a', topic: 'post topic', content: 'post content' }
]
let comments = [
{ id: '002a', post: '001a', parent: '001a', content: 'comment on post' },
{ id: '003a', post: '001a', parent: '002a', content: 'comment on comment' },
{ id: '004a', post: '001a', parent: '003a', content: 'comment on comments comment' }
]
let postComments = toNestedComments(comments, posts[0].id);
console.log(postComments);
function toNestedComments(comments, postId) {
const nestedComments = [];
const map = {};
for (let i = 0; i < comments.length; i++) {
map[comments[i].id] = i;
}
for (let comment of comments) {
if (comment.parent === postId) {
nestedComments.push(comment);
} else {
if (!comments[map[comment.parent]].hasOwnProperty('children')) {
comments[map[comment.parent]].children = [];
}
comments[map[comment.parent]].children.push(comment);
}
}
return nestedComments;
}
Retrieving the comments belonging to a specific parent only when you render that parent seems to me inefficient since each time you try to render the code will run through all your comments to find the ones belonging to that parent.
A better approach (in my opinion) is to add a property children to each posts/comment and populate it, as you have in your code. The difficulty here is to do this in way to minimize the number of times you have to 'loop' over all data.
One way is to build a lookup table for each post and comment like this:
const lookup = [...posts, ...comments].reduce((pre, cur) => {
cur.children = []; // Prepares an array to hold the children
pre[cur.id] = cur;
return pre;
}, {});
And then loop over the comments again, adding themselves to the parent we can read from the lookup table.
comments.forEach(c => lookup[c.parent].children.push(c));
Because of how javascript works in regards to objects, this will result in the items of the original posts and comments now having a children property populated with the comments for that element (and these comments having their own children).
Now you can recursively render these with a Post component and svelte:self.
{#each children as child}
<svelte:self {...child} />
{/each}
This solution only goes twice through the data to build this construction (once to make the lookup table, once to attach the comments) so is more efficient than searching the comments during render itself.
If your data is sorted in a way that a comment will always appear after it's parent, you could make this even better by pushing to the parent's children at the same time you build the map:
let map = [...posts, ...comments].reduce((pre, cur) => {
cur.children = [];
pre[cur.id] = cur;
if (cur.parent) pre[cur.parent].children.push(cur);
return pre;
}, {});
edit to fit with the data provided in the snippet
const lookup = [...posts].reduce((pre, cur) => {
// Either a post or a comment
const item = cur.post ?? cur.comment;
// Prepares an array to hold the children
item.children = [];
pre[cur.publicKey] = item;
return pre;
}, {});
comments.forEach(({ comment }) => lookup[comment.post].children.push(comment));
Not perfect, but a start to get an idea. This will put all comments on the post so comments do not have their own children.