sorting nested {{#each}} in Meteor? - meteor

In meteors guide i found the code below and I was wondering if todo.tags could be sorted somehow, maybe by a helpers method?
{{#each todo in todos}}
{{#each tag in todo.tags}}
<!-- in here, both todo and tag are in scope -->
{{/each}}
{{/each}}

One option would be to create a separate template helper which sorts all elements in your tags array. To sort the elements in an ascending order, you could use _.sortBy(list, iteratee, [context]), for example:
if (Meteor.isClient) {
Template.todos.helpers({
todos: function() {
return Todos.find();
},
sortedTags: function(todo) {
return _.sortBy(todo.tags, function(tag) {
return tag;
});
}
});
}
if (Meteor.isServer) {
Meteor.startup(function() {
if (Todos.find().count() === 0) {
Todos.insert({
name: "homework",
tags: ["school", "college", "university"]
});
}
});
}
<template name="todos">
{{#each todo in todos}}
{{todo.name}}
<ul>
{{#each tag in sortedTags todo}}
<li>{{tag}}</li>
{{/each}}
</ul>
{{/each}}
</template>
This can also be implemented with your sample data structure, provided in the comments:
{
"gtext":"Money",
"owner":"qDqGDaXjaHXNhX95u",
"username":"prsz",
"order":0,
"tasks":[
{
"taskName":"Test subtask1",
"taskOrder":3
},
{
"taskName":"Test subtask2",
"taskOrder":1
}
]
}
<template name="goals">
{{#each goal in goals}}
{{goal.gtext}}
<ul>
{{#each task in sortedTasks goal}}
<li>{{task.taskName}}</li>
{{/each}}
</ul>
{{/each}}
</template>
if (Meteor.isClient) {
Template.goals.helpers({
goals: function() {
return Goals.find();
},
sortedTasks: function(goal) {
return _.sortBy(goal.tasks, function(task) {
return task.taskOrder;
});
}
});
}
Here's a MeteorPad.

Related

Re-render templates on route change in Meteor 1.3

I'm using Flow Router and Blaze Renderer for a simple website (think blog / brochureware).
I'm using FlowRouter.path() to create links on my menu elements. The url changes as expected when these links are clicked and the action() method on the route is fired. However the templates don't seem to be refreshed and the template helpers aren't fired.
The route in my /lib/route.js file is
const about = FlowRouter.group({
prefix: '/about'
});
about.route('/:pageSlug/:subSectionSlug', {
action: (params) => {
console.log('action() fired :' + params.pageSlug + ' :' + params.subSectionSlug);
BlazeLayout.render( 'applicationLayout', {
main: 'basicPage',
});
},
name: 'pageSubsection',
});
Then my templates looks like -
<template name="applicationLayout">
{{> Template.dynamic template=main}}
</template>
<template name="basicPage">
<div id="pageWrapper">
...
<aside class="leftBar subSectionMenu">
{{> sidebar }}
</aside>
...
</div>
</template>
<template name="sidebar">
<ul class="pageMenu">
{{#each subSections }}
{{> sidebarItem}}
{{/each}}
</ul>
</template>
<template name="sidebarItem">
<a class="sidebarItemAnchor" href="{{ href }}">
<li class="sidebarItem .hoverItem {{#if isSelected }} selected {{/if}}">
{{title}}
<span class="sidebarArrow"></span>
</li>
</a>
</template>
With a simple helper to add the selected class to the li element -
Template.sidebarItem.helpers({
href() {
const subSection = this;
const params = {
pageSlug: subSection.pageSlug,
subSectionSlug: subSection.slug,
}
return FlowRouter.path('pageSubsection', params, {});
},
isSelected() {
const slug = FlowRouter.current().params.subSectionSlug;
console.log('running isSelected with ' + slug);
if (this.slug === slug) {
return true;
} else {
return false;
}
}
});
I think I must be misunderstanding how (and when) templates are rendered.
What do I need to do to re-render these templates when the route changes?
Flow Router was designed to work like this. It doesn't automatically re-render.
A simple fix is to add FlowRouter.watchPathChange(); into all Template helpers that depend on the params of a route.
So in this case update the sidebar.js -
Template.sidebarItem.helpers({
isSelected() {
FlowRouter.watchPathChange();
const slug = FlowRouter.current().params.subSectionSlug;
if (this.slug === slug) {
return true;
} else {
return false;
}
},
});
When that helper is used in the sidebarItem template it is now updated whenever the path changes.

Profile images not displaying in list

I'm trying to display profile images in a list of users. The list populates all names correctly however I'm having trouble displaying the profile images. The profile images have a separate collection and they're being stored using slingshot S3. The collection is publishing correctly because I can see all the data using meteortoys:allthings. I assume it my js file or how I'm trying to access them in the template. Let me know if you need more info.
Path: userList.js
Template.userList.helpers({
userList: ()=> {
return Meteor.users.find({_id: { $ne: Meteor.userId() }});
},
profileImg: function(){
return Files.find({userId: this._id});
}
});
Path: userList.html
<template name="userList">
{{#each userList}}
{{#if profileImg url}}
<img src="{{url}}" alt="{{url}}" class="profileImg">
{{/if}}
{{profile.firstName}} {{profile.familyName}}
{{/each}}
</template>
userList.js
Template.userList.helpers({
userList() {
return Meteor.users.find({ _id: { $ne: Meteor.userId() } });
},
profileImg() {
return Files.findOne({ userId: this._id });
},
});
userList.html
<template name="userList">
{{#each userList}}
{{#with profileImg}}
<img src="{{url}}" alt="{{url}}" class="profileImg">
{{/with}}
{{profile.firstName}} {{profile.familyName}}
{{/each}}
</template>
The with will change the context for the img such that it actually has a url property. Returning the result of findOne in profileImg is also necessary here.

Meteor: How should I write this Iron Router route?

I'm trying to rewrite my Iron Router adminhome router but when I click on the link to /admin/home I get the error below.
How should I rewrite this route?
Router.route('adminhome', {
layoutTemplate: 'adminlayout',
path:'/admin/home',
template: 'adminarea',
onBeforeAction: function() {
if (Meteor.loggingIn()) {
this.render(this.loadingTemplate);
} else if(!Roles.userIsInRole(Meteor.user(), ['admin'])) {
console.log('redirecting');
this.redirect('/');
} else {
this.next();
}
}
});
This is the error I get:
Exception from Tracker recompute function:
debug.js:41 Error: Expected template rendered with Blaze.render
at Object.Blaze.remove (view.js:679)
at DynamicTemplate.destroy (iron_dynamic-template.js:327)
at null._render (iron_layout.js:400)
at doRender (view.js:351)
at view.js:199
at Function.Template._withTemplateInstanceFunc (template.js:457)
at view.js:197
at Object.Blaze._withCurrentView (view.js:538)
at viewAutorun (view.js:196)
at Tracker.Computation._compute (tracker.js:323)
Here's my updated code. The problem might be related to the {{>yield}} line. That's just a guess though.
Router.route('adminhome', {
layoutTemplate: 'adminlayout',
path:'/admin/home',
template: 'adminarea',
onBeforeAction: function() {
if (Meteor.loggingIn()) {
//this.render(this.loadingTemplate);
this.render("loadingPage");
} else if(!Roles.userIsInRole(Meteor.user(), ['admin'])) {
console.log('redirecting');
this.redirect('/');
} else {
this.next();
}
}
});
<template name="adminTemplate">
{{#if isInRole "admin"}}
{{> adminarea}}
{{else}}
Must be admin to see this...
{{/if}}
</template>
<template name="adminarea">
{{>yield}}
</template>
In the line of your code...
this.render(this.loadingTemplate);
...replace this.loadingTemplate with the actual loading template name.
Eg.
this.render("LoadingTemplateName");

How to stop rendering template base on conditions in Meteor

Is there a way in Meteor to stop rendering template in some case? I found that this works:
Template.test.created = function () {
if (/* conditions */) {
this.view.template.renderFunction = function () {
return null;
};
}
};
But... maybe anyone can do this better?
It would be better to accomplish this within the template/helpers itself.
<template name="sometimesVisible">
{{#if visible}}
<!-- content here -->
{{/if}}
</template>
Template.sometimesVisible.helpers({
visible: function() {
// conditions here
}
]);

Publish not updating properly?

I'm having trouble getting Meteor.publish to update in response to a changing form field. The first call to publish seems to stick, so the query operates in that subset until the page is reloaded.
I followed the approach in this post, but am having no luck whatsoever.
Any help greatly appreciated.
In lib:
SearchResults = new Meteor.Collection("Animals");
function getSearchResults(query) {
re = new RegExp(query, "i");
return SearchResults.find({$and: [ {is_active: true}, {id_species: {$regex: re}} ] }, {limit: 10});
}
In client:
Session.set('query', null);
Template.searchQuery.events({
'keyup .query' : function (event, template) {
query = template.find('.query').value
Session.set("query", query);
}
});
Meteor.autosubscribe(function() {
if (Session.get("query")) {
Meteor.subscribe("search_results", Session.get("query"));
}
});
Template.searchResults.results = function () {
return getSearchResults(Session.get("query"));
}
On server:
Meteor.publish("search_results", getSearchResults);
Template:
Search for Animals
<body>
{{> searchQuery}}
{{> searchResults}}
</body>
<template name="searchQuery">
<form>
<label>Search</label>
<input type="text" class="query" />
</form>
</template>
<template name="searchResults">
{{#each results}}
<div>
{{_id}}
</div>
{{/each}}
</template>
Update [WRONG]
Apparently, the issue is that the collection I was working with was (correctly) generated outside of Meteor, but Meteor doesn't properly support Mongo's ObjectIds. Context here and related Stackoverflow question.
Conversion code shown there, courtesy antoviaque:
db.nodes.find({}).forEach(function(el){
db.nodes.remove({_id:el._id});
el._id = el._id.toString();
db.nodes.insert(el);
});
Update [RIGHT]
So as it turns out, it was an issue with RegExp / $regex. This thread explains. Instead of:
function getSearchResults(query) {
re = new RegExp(query, "i");
return SearchResults.find({$and: [ {is_active: true}, {id_species: {$regex: re}} ] }, {limit: 10});
}
At the moment, one needs to do this instead:
function getSearchResults(query) {
// Assumes query is regex without delimiters e.g., 'rot'
// will match 2nd & 4th rows in Tim's sample data below
return SearchResults.find({$and: [ {is_active: true}, {id_species: {$regex: query, $options: 'i'}} ] }, {limit: 10});
}
That was fun.
PS -- The ddp-pre1 branch has some ObjectId functionality (SearchResults = new Meteor.Collection("Animals", {idGeneration: "MONGO"});)
Here's my working example:
UPDATE the original javascript given was correct. The problem, as noted in the comments, turned out to be that meteor doesn't yet support ObjectIds.
HTML:
<body>
{{> searchQuery }}
{{> searchResults}}
</body>
<template name="searchQuery">
<form>
<label>Search</label>
<input type="text" class="query" />
</form>
</template>
<template name="searchResults">
{{#each results}}
<div>
{{id_species}} | {{name}} - {{_id}}
</div>
{{/each}}
</template>
Javascript:
Animals = new Meteor.Collection("Animals");
function _get(query) {
re = new RegExp(query, "i");
console.log("rerunning query: " + query);
return Animals.find({$and: [ {is_active: true}, {id_species: {$regex: re}} ] }, {limit: 10});
};
if (Meteor.isClient) {
Session.set("query", "");
Meteor.autosubscribe(function() {
Meteor.subscribe("animals", Session.get("query"));
});
Template.searchQuery.events({
'keyup .query' : function (event, template) {
query = template.find('.query').value
Session.set("query", query);
}
});
Template.searchResults.results = function () {
return _get(Session.get("query"));
}
}
if (Meteor.isServer) {
Meteor.startup(function() {
if (Animals.find().count() === 0) {
Animals.insert({name: "panda", is_active: true, id_species: 'bear'});
Animals.insert({name: "panda1", is_active: true, id_species: 'bearOther'});
Animals.insert({name: "panda2", is_active: true, id_species: 'bear'});
Animals.insert({name: "panda3", is_active: true, id_species: 'bearOther'});
}
});
Meteor.publish("animals", _get);
}

Resources