How to use handlebars to output nested mongodb docs - mongodb

I have a mongodb called categories and structured as:
{ (id), name, tree }
and data:
{ (639...bc78), "ABC", null},
{ (63d...c891), "DEF", null},
{ (63f...718d), "yyy", ",ABC,"}
'yyy' is a child of 'ABC'.
I can output ALL the data using handlebars
{{#each categories}}
{{#if tree}}
<tr><td> </td><td> </td><td>TREE:{{tree}}</td></tr>
{{else}}
<tr><td>{{_id}}</td><td>{{name}}</td></tr>
{{/if}}
{{/each}}
This outputs:
639...bc78 ABC
63d...c891 DEF
63f...718d yyy TREE:ABC
What I would like to achieve is interleaved output of the child docs (i.e. 'yyy' outputs after its parent 'ABC'):
639...bc78 ABC
TREE: yyy
63d...c891 DEF
I am fairly new to Express and very new to express-handlebars and cannot find any documentation or suggestions that would provide this functionality. Any ideas/code would be greatly appreciated.

It will be challenging to get the output you want by performing the logic in the Handlebars template. You would need to write a custom helper and that helper would need to do a lot of looping because in each iteration of the outer category loop it would need to do another category loop to find the children.
The better approach would be to map your data to a different data structure before you pass it to your template. In this structure, the top-level category objects would "own" their own arrays of children. For example:
const categoriesByName = categories.reduce((acc, category) => {
if (!category.tree) {
acc[category.name] = { ...category, children: [] };
} else {
const parent = category.tree.replace(/,/g, '');
if (acc[parent]) {
acc[parent].children.push(category);
} else {
// Note: We are assuming that children _always_ exist higher
// in the array than their parents.
// If no parent name has been indexed, we lose the child.
console.error(`No parent with name: ${name}`);
}
}
return acc;
}, {});
This will produce a structure like:
{
"ABC": {
"_id": "639...bc78",
"name": "ABC",
"tree": null,
"children": [
{
"_id": "63f...718d",
"name": "yyy",
"tree": ",ABC,"
}
]
},
"DEF": {
"_id": "63d...c891",
"name": "DEF",
"tree": null,
"children": []
}
}
Note: This structure does not get deeper than two-levels - ie., it doesn't allow children to have children.
With this structure, our template needs to iterate through the top-level categories and, for each, include an inner-loop for any children they might have:
{{#each categories}}
<tr>
<td>{{_id}}</td>
<td>{{name}}</td>
</tr>
{{#each children}}
<tr>
<td></td>
<td>TREE: {{name}}</td>
</tr>
{{/each}}
{{/each}}
Here is an example fiddle.

Related

Mongo find() returns [object Object]

In meteor I've created a database array. The following is the contents:
meteor:PRIMARY> db.games.find()
{ "_id" : "ceg9JJ3u5abwqeyk7", "board" : [ [ 0, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ] }
In my client/main.js file inside my template helper I have:
Template.hello.helpers({
counter() {
return Template.instance().counter.get();
},
game() {
return {
game: Games.find()}
}
});
In my main.html I have the following template:
<template name="hello">
{{game}}
</template>
My output on the browser is:
[object Object]
But I want to have the contents of my array (e.g. "0" ) put in the browser not "object".
How do I do this?
You are not properly iterating over your query.
Template.hello.helpers({
game() {
return Games.find()
}
});
main.html:
<template name="hello">
{{#each game}}
Boards: {{board}}
{{/each}}
</template>
Explanation based on your comment:
There are 3 different ways to find records from your collection.
Collection.findOne(): Returns only 1 record as an object
Collection.find().fetch(): Returns all the records as an array of objects
Collection.find(): Returns a cursor (which is a function)
Please use your browser console to see the difference between each of these using below statements to get a better understanding:
console.log(Games.findOne());
console.log(Games.find().fetch());
console.log(Games.find());
All of these will return you your entire board field data as all the data is stored as a single record.
So you have to store this data in one of the following ways in-order for you to filter the data as per your requirement.
Method 1:
Store your data in the below format as individual record for each game:
{
"_id" : "ceg9JJ3u5abwqeyk7",
"name": "Game-1",
"score": [ 0, 0, 0 ]
},{
"_id" : "bzv778zv6qge7xc8",
"name": "Game-3",
"score": [ 0, 0, 0 ]
},{
"_id" : "eji3ds9jo8yhs7739",
"name": "Game-3",
"score": [ 0, 0, 0 ]
},
You can now display the data using below code:
<template name="hello">
{{#each game}}
Game Name: {{name}}, Score: {{score}} <br/>
{{/each}}
</template>
If you want to display only the "Game-3" score, the you can filter while fetching the data using find:
<template name="hello">
{{#each game}}
Game Name: Game-3, Score: {{score}}
{{/each}}
</template>
Template.hello.helpers({
game() {
return Games.find({name:"Game-3"});
}
});
Method 2:
Store your data in the below format:
{
"_id" : "ceg9JJ3u5abwqeyk7",
"board" : [
{
"name": "Game-1",
"score": [ 0, 0, 0 ]
},
{
"name": "Game-2",
"score": [ 0, 0, 0 ]
},
{
"name": "Game-3",
"score": [ 0, 0, 0 ]
},
]
}
You can display the data using below code:
<template name="hello">
{{#each game}}
Boards: <br/>
{{#each board}}
Game Name: {{name}}, Score: {{score}} <br/>
{{/each}}
{{/each}}
</template>
If you want to display only the "Game-3" score, the you can filter using a helper as shown below:
<template name="hello">
{{#each game}}
Boards: <br/>
{{#each board}}
{{#if isThirdGame}}
Game Name: Game-3, Score: {{score}}
{{/if}}
{{/each}}
{{/each}}
</template>
Template.hello.helpers({
game() {
return Games.find()
},
isThirdGame() {
return this.name === "Game-3" ? true : false;
},
});

Meteor: How to subscribe/show usernames when this.userId is published

Note: Whole sourcecode can be found here:
https://github.com/Julian-Th/crowducate-platform/
I have the following pub functions:
Meteor.publish('editableCourses', function () {
return Courses.find({"canEditCourse": { $in: [ this.userId ] } });
});
Meteor.publish("userData", function () {
if (this.userId) {
return Meteor.users.find({_id: this.userId},
{fields: {'realname': 1, 'username': 1, 'gender': 1, 'language': 1, 'biography': 1 }})
}
});
Meteor.publish("allUsernamesExceptCurrent", function () {
// Get current user
var currentUserId = this.userId;
// Get all users except current,
// only get username field
var users = Meteor.users.find(
{_id: {$ne: currentUserId}},
{fields: {username: 1}}
);
return users;
});
Here's the template:
<template name="myTeachingCourses">
<button class="btn btn-primary js-create-course"><i class="fa fa-plus"></i> Create a Course</button>
<hr>
<div class="row">
<ul class="thumbnails list-unstyled">
{{#each courses}}
{{>courseCard}}
{{/each}}
</ul>
</div>
</template>
The helper with subscription:
Template.myTeachingCourses.helpers({
'courses': function(){
return Courses.find();
}
});
Template.myTeachingCourses.created = function () {
// Get reference to template instance
var instance = this;
// Subscribe to all published courses
instance.subscribe("editableCourses");
};
The problem: The template doesn't render the created courses. A course creator can't see his own courses or add other collaborators. I wonder if the reason might be, because canEditCourse is now an array with usernames and not with IDs.
My JSON for Courses:
{
"_id" : "hRX8YABpubfZ4mps8",
"title" : "Course Title here",
"coverImageId" : "Qojwbi2hcP2KqsHEA",
"author" : "Name",
"keywords" : [
"test",
"meteor"
],
"published" : "true",
"about" : "Testing",
"canEditCourse" : [
"User1"
],
"createdById" : "zBn6vtufK2DgrHkxG",
"dateCreated" : ISODate("2015-12-29T21:42:46.936Z")
}
Via this question I found out that storing usernames in the array is not a good idea for now obvious reasons. In the beginning, I actually stored IDs instead of usernames but then changed it because to use the username for autocomplete with typeahead and rendering the IDs of all collaborators is not good thing to do. What is the best way to fix it? In other words, how is it possible to render all created courses.
So, as you answered yourself, your Courses collection field canEditCourse has an array of usernames, not userId, so the query will not return anything.

Sorting a Mongo cursor such that certain documents Ids come first

I have a Mongo collection that I need to search, returning the results sorted as follows:
first by whether or not the document id is present in an array that I pass in.
then by another field in the document.
My use-case is that the user has a set of 'favourited' items that I wish to display first in the list. I'm storing that list of ids in the user's document.
Is there a way to handle this?
As #Challet suggested above, with the addition of templating to avoid having to concatenate the two search results and keeping both lists reactive in the process:
html:
<template name="myTemplate">
{{#each favorites}}
{{> detail}}
{{/each}}
{{#each nonFavorites}}
{{> detail}}
{{/each}}
</template>
<template name="detail">
... layout details here ...
</template>
js:
var idsOfFavorites = []; // however you maintain this
Template.myTemplate.helpers({
favorites: function(){
return MyCollection.find({ _id: { $in: idsOfFavorites }},
{ sort: { createdAt: -1 }}); // sorting by createdAt but could be any field
},
nonFavorites: function(){
return MyCollection.find({ _id: { $nin: idsOfFavorites }},
{ sort: { createdAt: -1 }});
}
});

How do I get only part of my mongo document from inside my Meteor helper?

I have a document with an array of transactions. I'm trying to render a receipt template with the correct transaction's data. I just moved my subdocument into an array and I need to convert these helpers to reflect the change, but I'm not sure how to do that.
Here is my js file snippet.
Template.Gift.helpers({
displayReceipt: function () {
var transaction_guid = Session.get('transaction_guid');
var transaction_status = this.recurring.transactions[transaction_guid].status;
return (transaction_status === 'succeeded');
}
});
And here is my html file snippet.
<div class="Receipt">
{{#if displayReceipt}}
{{> Receipt}}
{{else}}
{{/if}}
</div>
How can I change this to pull the correct transaction from the array?
Here is what my object looked like originally.
"transactions": {
"TX1234": {
"guid": "TX1234",
"amount": 102,
"email_sent": false,
"status": "succeeded"
},
"TX1235": {
"guid": "TX1235",
"amount": 102,
"email_sent": true,
"status": "failed"
}
}
Here is the new array style
transactions: [
{
"guid": "TX1234",
"amount": 102,
"email_sent": false,
"status": "succeeded"
},
{
"guid": "TX1235",
"amount": 102,
"email_sent": true,
"status": "failed"
}
]
What you're accessing doesn't look like a Mongo (Minimongo) instance. It looks like you've pulled the transactions directly into a Javascript object, which seems to defeat the purpose of using Minimongo.
Anyway, this would be the query you'd use, if the transaction was in Minimongo:
var item = SomeCollection.findOne({"transactions.guid": transaction_guid});
var transaction = _.findWhere(item.transactions, { guid: transaction_guid });
The query returns the document with that whole array, because Meteor doesn't support MongoDB's $elemMatch projection yet. So you'd have to filter for the right element yourself, as above. I'm using underscore's findWhere function in this example.

How do I implement Mongo DB query to find key in all objects inside an array?

After looking into several books, the mongo db reference and other stackoverflow questions, I can't seem to get this query right.
I have the following data structure:
Posts collection:
{
"content" : "...",
"comments" : [
{
"author" : "joe",
"score" : 3,
"comment" : "nice post"
},
{
"author" : "bob",
"score" : 5,
"comment" : "nice"
}]
}
What I'm trying to get is all of the author names inside each of the objects of the array inside a Handlebars helper, not in the console. So I would have something like:
...
commentsAuthors: function() {
return Collection.find({...});
}
UPDATE:
I decided to reduce my array to only an Array of strings which I later queried like this:
New Array:
{
"content" : "...",
"comments" : ["value1", "value2", "..."]
}
MeteorJS Handlebars helper:
Template.courseEdit.helpers({
comments: function(){
var cursor = Courses.find({"_id": "8mBATtGyyWsGr2PwW"}, {"comments": 1, "_id": 0}).fetch();
return cursor.map(function(doc, index, cursor){
var comment = doc.comments;
console.log(comment);
return comment;
});
}
});
At this point, I'm able to render the Whole array in my view with this {{#each}}...{{/each}}:
<ul class="list-unstyled">
{{#each comments}}
<li class="pl-14"><i class="icon-checkmark"></i> {{this}}</li>
{{/each}}
</ul>
However I'm getting the whole array in one single list item.
How can I create a separate list item for each of my Array strings?
Thanks in advance.
Should be as easy as (replace posts with your collection name):
db.posts.distinct("comments.author")
Or if you need it per post:
db.posts.aggregate([
{ $unwind: "$comments" },
{ $group: { "_id": "$_id", authors: { "$addToSet": "$comments.author"} } }
])
You can get only certain fields by limiting fields
db.collection.find({"content" : "..."},{"comments.author":1})