I have items with a category field and name field, e.g.:
{ category: 'CategoryOne', name: "ItemOne" },
{ category: 'CategoryOne', name: "ItemTwo" },
{ category: 'CategoryTwo', name: "ItemThree" },
... etc
What I would like to do is display these under a heading for the category.
I am new to meteor, and having quite the time doing two things:
(1) Getting a reactive list of categories, or
(2) Iterating through the items, displaying them grouped by category.
I'm not sure what is the correct Meteor approach here.
Unfortunately minimongo doesn't have support for aggregation yet so this is a bit difficult. The following is how I would approach it.
First create 2 template helpers. The first just puts together a list of the categories and returns an array of category names, The second takes the category name as a parameter and returns a cursor of all of the records in that category.
Template.categories.helpers({
categories: function(){
var added = [];
return Items.find().map(function (item) {
if(_(added).indexOf(item.category) === -1){
return item.category;
}
});
},
categoryItems: function(category){
return Items.find({category:category});
}
});
Next the template needs nested {{#each}} blocks with the first one iterating over the categories array and passing the category names to the next each as the parameter of the next helper.
<template name="categories">
{{#each categories}}
<h1>{{this}}</h1>
<ul>
{{#each items this}}
<li>{{name}}</li>
{{/each}}
</ul>
{{/each}}
</template>
Related
I am new to meteor, and have a basic understanding of what is going on, but I am stuck with this example (the problem has been simplified as much as possible):
I have a template, and a child template:
<template name="test">
{{#each items}}
{{> testItem}}
{{/each}}
{{#each items}}
{{> testItem}}
{{/each}}
</template>
<template name="testItem">
<div {{ b "click: toggle"}}>{{value}}</div>
</template>
Template.test.viewmodel({
items: [],
onCreated: function() {
this.items().push({ value: 'test' });
}
})
Template.testItem.viewmodel({
toggle: function() {
this.value("changed");
}
});
The thing here is we have a single array of items in the viewmodel, and we render it through a child template multiple times.
When we toggle the item, it only toggles the single item template, not the other representation of it. It is behaving like it is copying the value, or some sort of scoping is taking place.
My expectation would be the second item to also change, but this is not the case - what am I missing, or misunderstanding here?
EDIT - Additional Investigation
If I change the item through the parent, and notify it has changed, the changes propogate throughout the child templates
Template.testItem.viewmodel({
toggle: function () {
this.parent().items()[0].value = "changed";
this.parent().items().changed();
}
});
Thanks!
You're right, when you do this.value("changed"); you're changing the value of the testItem view model, not the parent array. If you're going to modify the properties of objects in an array I highly recommend you use a client side Mongo collection instead. It will save you a lot of headaches.
Items = new Mongo.Collection(null);
Template.test.viewmodel({
items: function() {
return Items.find();
},
onCreated: function() {
Items.insert({ _id: "1", value: 'test' });
}
})
Template.testItem.viewmodel({
toggle: function() {
Items.update({ _id: this._id() }, { value: 'changed' });
}
});
btw, I rarely check SO. You will get quicker responses on viewmodelboard.meteor.com
I'm trying to find a nice, Meteor-style way to handle this issue.
I have a set of Mongo documents sorted by date that I can easily display in a list:
<template name="logbook">
<h1>{{title}}</h1>
<div>
{{#each entries}}
{{> Entry}}
{{/each}}
</div>
</template>
Now, each time the year changes, I'd like to output it, so that I get something like this:
2014
doc 1
doc 2
2013
doc 3
doc 4
doc 5
etc.
This this is Meteor, I'd like the list to be reactive. If a new document arrives then it should be inserted in the right place in the list, and the year added if necessary.
Can anyone suggest a sensible approach to handle this?
You could probably use helper that will check if year is the same as in the last record, if not - he will output it, something like
<template name="Entry">
{{year}}
{{data}}
</template>
And in js
year: function(){
//operations that will return year to some variable, for example year_variable
if(global_variable===undefined){
global_variable=year_variable;
return year_variable;
}
if(global_variable===year_variable) return false;
else return year_variable;
}
There is no need to make it global tho, you can use sessions for it
This may not be precisely what you are looking for with respect to naming conventions, but it will give you an idea of how I would approach this problem:
Create a list of unique years
For each year, render a template (logbook)
Within each logbook, iterate over all of the entries for that year
Here is a complete working solution:
app.html
<body>
{{#each years}}
{{> logbook}}
{{/each}}
</body>
<template name="logbook">
<h2>{{year}}</h2>
<ol>
{{#each entries}}
<li>{{text}}</li>
{{/each}}
</ol>
</template>
app.js
if (Meteor.isClient) {
// create a client-side collection for testing
Entries = new Mongo.Collection(null);
Meteor.startup(function() {
// insert some data in the wrong order to test sorting
Entries.insert({text: 'doc6', date: new Date('1/3/2013')});
Entries.insert({text: 'doc4', date: new Date('1/1/2013')});
Entries.insert({text: 'doc5', date: new Date('1/2/2013')});
Entries.insert({text: 'doc3', date: new Date('1/3/2014')});
Entries.insert({text: 'doc1', date: new Date('1/1/2014')});
Entries.insert({text: 'doc2', date: new Date('1/2/2014')});
});
Template.body.helpers({
years: function() {
// return a list of unique sorted objects with a year field
return _.chain(Entries.find().fetch())
// pluck out the dates
.pluck('date')
// convert each date to a year
.map(function(date) {return date.getFullYear();})
// sort the years in reverse order
.sortBy(function(year) {return -year;})
// find only the unique values
.uniq(true)
// '2014' -> {year: '2014'}
.map(function(year) {return {year: year};})
.value();
}
});
Template.logbook.helpers({
entries: function() {
var year = this.year;
var entries = Entries.find({}, {sort: {date: 1}}).fetch();
// return a list of entries only for this year
return _.filter(entries, function(entry) {
return entry.date.getFullYear() === year;
});
}
});
}
I have three lists on the same page which I want to fill with list-items. The list items are associated to the lists by a field called listId
My publications:
Meteor.publish('lists', function(options) {
return Lists.find({}, options);
});
Meteor.publish('listItems', function(listId) {
return Cards.find({listId: listId});
});
My lists-page.js (this._id param is passed with iron-router.):
Template.listsPage.helpers({
lists: function(){
return Lists.find({listsPageId: this._id});
},
listItems: function(listId){
//??
return ListItems.find({listId: listId})
}
});
My lists-page.html:
<template name="listsPage">
{{#each lists}}
<ul>
<li>{{title}}</li>
{{#each listItems}}
<li>{{listItemTitle}}</li>
{/each}
{{#each lists}}
</template>
Any help is much appreciated!
As a general recommendation, I would avoid collections and publish functions which have generic names like "lists" and "items". I find it makes code incredibly hard to understand. That being said, I think the confusion here is about the name associated with a publish function and the name of the underlying collection.
The name of the publish function (listItems in this case) is used only to facilitate the activation by a subscription, and has no other meaning. The function publishes a set of documents in the Cards collection to the client. Those documents should subsequently be retrieved from the client's local database using the Cards collection.
So I think the code you are looking for is:
listItems: function() {
return Cards.find({listId: this._id});
}
The context in which the listItems helper is run is that of a List document. So the id for listId should be this._id. Let me know if that doesn't work for some reason.
Say you have a bunch of blog posts, and each post has a "title" and "category". How would you render all post titles on a single page, where there is a table for each group of posts that have the same "category" values?
I'm starting by sorting by category, so the posts in the same category are grouped together in the cursor:
Template.postLists.posts = function() {
return Posts.find({}, {sort:{category:1}});
}
But I'm struggling with iterating through this list in a template via {{#each}}, and using Handlebars to detect when I reach a new "category", so that I can start a new , and then end the when I'm at the end of the category.
Am I coming at this the wrong way, or is there an easy way to do this?
The solution I went with was that in my Template handler, instead of returning a cursor using Posts.find(), I created a JSON object that has a structure that can be processed by a handlebars template (an array of category objects, where each category has an array of posts):
Template.postLists.categorizedPosts = function() {
var allPosts = Posts.find({}, {sort:{category:1}}).fetch();
// Then I iterate over allPosts with a loop,
// creating a new array of this structure:
// for ...
var catPosts = [ { category:"cat1", posts: [ {post1}, {post2} ] },
{ category:"cat2", posts: [ {post3}, {post4}, {post5} ] },
// etc...
];
// end loop
return catPosts;
The the template is something like this (but with tables instead of UL, just using UL for a cleaner demo here):
{{#each categorizedPosts}}
{{category}}
<ul>
{{#each posts}}
<li>{{posts.title}}</li>
{{/each}}
</ul>
{{/each}}
Note that when you return an object like this (instead of a cursor object that Posts.find() returns), Meteor's templating engine loses the ability to intelligently detect when only one of the objects in the collection has changed, and patching the DOM instead of completely re-rendering the template. So in this case, the template is completely re-rendered even if a single Posts object is updated in the DB. That's the downside. But the upside is that it works ;)
I'm trying to wrap my head around the meteor dependencies and reactive variables.
I have two select boxes. One lists a category (fruit, vegetables, poultry, etc) the second will list the sub category (apples, pears, grapes, etc).
I'd like when the user changes the category dropdown to display and populate the subcategory dropdown.
I know I can watch for Template.action_form.events ={'change #category'}... but I'm not sure what steps to take from here. One thought (hack) is to output all the subcategories to a multidimensional array and use jquery to manage it. I have to think there is a smarter way to to do this with meteor.
for the category dropdown I have something like this:
Template.action_form.category = function(id){
return Category.find();
}
I'm not sure how to setup the template for the subcategory...right now I have this (not working)
Template.action_form.subcategory = function(parent){
if (document.getElementById(parent)){
category = document.getElementById(parent).value;
return Subcategories.find({category_id:parent});
}
}
The HTML/Template looks like this:
<template name="action_form">
<select id="category" class="action-selects">
{{#each category _id}}
<option value="{{_id}}">{{name}}</option>
{{/each}}
</select>
<select id="subcategory" class="action-selects">
{{#each subcategory "category"}}
<option value="{{_id}}">{{name}}</option>
{{/each}}
</select>
<template>
Thanks in for any pointers you all can offer.
if you want to use the whole reactivity magic of meteor for this, you could set an Session variable if the first select changes.
Template.action_form.events = {
'change #category': function(evt) {
Session.set("selected_category", evt.currentTarget.value);
}
}
Your subscription of Subcategories passes the selected category as a parameter into the servers publish method.
// Client
Meteor.autosubscribe(function () {
Meteor.subscribe("subcategories",Session.get("selected_category"));
}
// Server
Meteor.publish("subcategories", function(selectedCategory) {
Subcategories.find({category_id: selectedCategory})
});
The template for subcategories than displays all Subcategories if finds.
Template.action_form.subcategory = function(parent){
Subcategories.find();
};
You could, of course, publish all Subcategories at once (no idea how many you'll have there) and filter the subcategories in the client, not in the subscribe/publish methods.
Template.action_form.subcategory = function(parent){
Subcategories.find({category_id: Session.get("selected_category")});
};