Summary:
Child categories nested inside of Parent Categories are not getting rendered in a Meteor template.
Details:
Consider a data model for 'Category' as such:
// Model Schema
Category {
idCategory : 20, (id of the category itself)
idCategoryParent : 0, (idCategory of our parent category)
defaultLabel : "Movies" (our label)
}
There are parent categories and child categories. Parent categories have an idCategoryParent property value of 0. Child categories store the idCategory of their parents as their idCategoryParent property. I'm trying to loop through a collection of these Categories and render them in the following way:
<b>Movies</b> // parent category is in bold
<ul> // child categories are rendered as an unordered list
<li>Horror</li>
<li>Comedy</li>
<li>Action</li>
<li>Drama</li>
</ul>
<b>Music</b>
<ul>
<li>Rock</li>
<li>Classical</li>
<li>Ambient</li>
</ul>
However, this is what I actually get:
<b>Movies</b>
<ul> // empty...
</ul>
<b>Music</b>
<ul>
</ul>
Source Code:
// How we get the 'categories_parents' data
Template.content.categories_parents = function (){
/*
* Get all parent categories (categories with an idCategoryParent of 0)
*/
var parents = Categories.find({idCategoryParent:0});
var pCount = parents.count();
for (var i = 0; i < pCount; i++){
var pId = parents.db_objects[i]['idCategory'];
/*
* Get all child categories of the parent (categories with
* an idCategoryParent equal to value of parent category's idCategory).
*/
var children = Categories.find({idCategoryParent:pId});
var cCount = children.count();
/*
* Assign the child categories array as a property of the parent category
* so that we can access it easily in the template #each expression
*/
parents.db_objects[i]['children'] = children;
}
return parents;
}
// This is our template
<template name="content">
<h1>Categories</h1>
{{#each categories_parents}}
<b>{{defaultLabel}}</b><br />
<ul>
{{#each children}}
<li>{{defaultLabel}}</li>
{{/each}}
</ul>
{{/each}}
</template>
Other template configurations I have tried in troubleshooting:
{{#each children}}
<li>A Child Exists Here</li> // Even this never rendered... no children?
{{/each}}
Any clues as to why this is happening would be appreciated.
Summary:
Child categories nested inside of Parent Categories are not getting rendered in a Meteor template.
Details:
Consider a data model for 'Category' as such:
// Model Schema
Category {
idCategory : 20, (id of the category itself)
idCategoryParent : 0, (idCategory of our parent category)
defaultLabel : "Movies" (our label)
}
There are parent categories and child categories. Parent categories have an idCategoryParent property value of 0. Child categories store the idCategory of their parents as their idCategoryParent property. I'm trying to loop through a collection of these Categories and render them in the following way:
<b>Movies</b> // parent category is in bold
<ul> // child categories are rendered as an unordered list
<li>Horror</li>
<li>Comedy</li>
<li>Action</li>
<li>Drama</li>
</ul>
<b>Music</b>
<ul>
<li>Rock</li>
<li>Classical</li>
<li>Ambient</li>
</ul>
However, this is what I actually get:
<b>Movies</b>
<ul> // empty...
</ul>
<b>Music</b>
<ul>
</ul>
Source Code:
// How we get the 'categories_parents' data
Template.content.categories_parents = function (){
/*
* Get all parent categories (categories with an idCategoryParent of 0)
*/
var parents = Categories.find({idCategoryParent:0});
var pCount = parents.count();
for (var i = 0; i < pCount; i++){
var pId = parents.db_objects[i]['idCategory'];
/*
* Get all child categories of the parent (categories with
* an idCategoryParent equal to value of parent category's idCategory).
*/
var children = Categories.find({idCategoryParent:pId});
var cCount = children.count();
/*
* Assign the child categories array as a property of the parent category
* so that we can access it easily in the template #each expression
*/
parents.db_objects[i]['children'] = children;
}
return parents;
}
// This is our template
<template name="content">
<h1>Categories</h1>
{{#each categories_parents}}
<b>{{defaultLabel}}</b><br />
<ul>
{{#each children}}
<li>{{defaultLabel}}</li>
{{/each}}
</ul>
{{/each}}
</template>
Other template configurations I have tried in troubleshooting:
{{#each children}}
<li>A Child Exists Here</li> // Even this never rendered... no children?
{{/each}}
Any clues as to why this is happening would be appreciated.
Share Improve this question edited Dec 6, 2012 at 19:45 eric asked Dec 6, 2012 at 19:16 ericeric 4,95111 gold badges43 silver badges56 bronze badges3 Answers
Reset to default 15Your model is kind of iffy... Consider
{name:"Category name", parent:"_id of parent category"}
Okay, that's a lot simpler. To create a category.
var moviesId = Categories.insert({name:"Movies"});
Categories.insert({name:"Horror",parent:moviesId});
That was easy enough. Now, to render in a way that {{#each}}
works:
Template.categories.categories = function(parent) {
if (parent) {
return Categories.find({parent:parent}).fetch();
} else {
return Categories.find({parent:{$exists:false}});
}
}
You might be seeing where this is going...
<template name="categories">
{{#each categories}}
<ul>{{name}}
{{#each categories _id}}
<li>{{name}}</li>
{{/each}}
</ul>
{{/each}}
</template>
Now I'm not sure if the {{#each}}
block helper can take a function argument when it calls another helper. If it doesn't...
Template.categories.categories = function() {
return Categories.find({parent:{$exists:false}}).map(function(parentCategory) {
return _.extend(parentCategory,
{children:Categories.find({parent:parentCategory._id}).fetch()});
});
}
That's a real doozy. It returns parent categories with a new "children" list property, that contains all the children categories. Now you can do:
<template name="categories">
{{#each categories}}
<ul>{{name}}
{{#each children}}
<li>{{name}}</li>
{{/each}}
</ul>
{{/each}}
</template>
Clever, eh?
I don't know about db_objects, when I try and access that property on a cursor (which is what find()
returns), it's null
.
You could fetch the items that matches your query instead, and then do your iteration:
Template.content.categories_parents = function (){
var parents = Categories.find({idCategoryParent:0}).fetch(); // Returns an array.
for (var i = 0; i < parents.length; i++){
var pId = parents[i]['idCategory'];
var children = Categories.find({idCategoryParent:pId});
// No need for array here, cursor is fine.
parents.db_objects[i]['children'] = children;
}
return parents;
}
I'm new at this myself, so maybe there's a more efficient way of doing it, but I don't know it currently.
Update after Eric's ment.
The js file looks like this:
Categories = new Meteor.Collection("categories");
if (Meteor.isClient) {
Template.categories.categories = function () {
var parents = Categories.find({idCategoryParent:0}).fetch();
for (var i = 0; i < parents.length; i += 1) {
var pId = parents[i]['idCategory'];
var children = Categories.find({idCategoryParent:pId});
parents[i]['children'] = children;
}
return parents;
};
}
if (Meteor.isServer) {
Meteor.startup(function () {
Categories.remove({});
var data = [
{idCategoryParent: 0, idCategory: 1, label: "Movies"},
{idCategoryParent: 1, idCategory: 0, label: "Science Fiction"},
{idCategoryParent: 1, idCategory: 0, label: "Drama"},
{idCategoryParent: 0, idCategory: 2, label: "Music"},
{idCategoryParent: 2, idCategory: 0, label: "Jazz"},
{idCategoryParent: 2, idCategory: 0, label: "Piano"}
];
for (var i = 0; i < data.length; i += 1) {
Categories.insert(data[i]);
}
});
}
The html file looks like this:
<head>
<title>nested_template</title>
</head>
<body>
{{> categories}}
</body>
<template name="categories">
<h1>Categories</h1>
{{#each categories}}
<b>{{label}}</b>
<ul>
{{#each children}}
<li>{{label}}</li>
{{/each}}
</ul>
{{/each}}
</template>
It works just fine for me.
Solved.
My solution was to remove the {{#each}} logic from the template and replace it with a single handlebars helper expression, and pass back the needed html all at once. The html is generated from data in the cursor of the Categories collection.
Not so sure about having all this html in my logic though -- is this bad design? If so I'll defer to a better answer.
// This is the new template
<template name="content">
<h1>Categories</h1>
{{listCategories}}
</template>
// Handlebars helper
Handlebars.registerHelper('listCategories', function() {
var parents = Categories.find({idCategoryParent:0});
var countParents = parents.count();
var string = '';
// iterate over each parent category
for(m = 0; m < countParents; m++){
// Get the parents category id
var pId = parents.db_objects[m].idCategory;
var children = Categories.find({idCategoryParent:pId});
var count = children.count();
/*
* Build the Parent Category html
* Example: <b>Movies</b><ul>
*/
string = string + '<b>' + parents.db_objects[m].defaultLabel + '</b><ul>';
// iterate over each child category
for(var i = 0; i < count; i++){
/*
* Build the child category html
* Example: <li>Horror</li>
*/
string = string + '<li>' + children.db_objects[i]['defaultLabel'] + '</li>';
}
// Close up the unordered list
string = string + '</ul>';
}
// Return the string as raw html
return new Handlebars.SafeString(string);
});
// Rendered out the result correctly like so:
<b>Movies</b>
<ul>
<li>Horror</li>
<li>Comedy</li>
<li>Action</li>
<li>Drama</li>
</ul>
<b>Music</b>
<ul>
<li>Rock</li>
<li>Classical</li>
<li>Ambient</li>
</ul>