I am trying to build a moderately reusable complex component in Meteor. It will be included in multiple templates with similar data structures and I am trying to achieve something like an Angular Directive.
The data context looks something like this:
var post = {
title: 'A test post',
author: 'Joe Bloggs',
bookmarked: true,
bookmarkCount: 25
}
In the HTML template I have something like this:
<template name="postDetail">
<div class="jumbotron">
<h3>{{title}}</h3>
{{> footerLinks}}
</div>
</template>
The footerLinks template is the reusable component I am trying to now build. I'd like have it as self-contained as possible, with it's own js logic. A simplified version is:
<template name="footerLinks">
{{author}} ยท {{formattedBookmarkCount}}
</template>
The {{author}} comes directly from the data context. I would like to use a function to build the text for the bookmark counts. Surprisingly this doesn't work, it doesn't even return the default.
Template.footerLinks.helpers({
updatedAt: 'wow',
formattedBookmarkCount: function () {
switch (bookmarkCount) {
case 0:
return "No bookmarks";
case 1:
return "1 bookmark";
default:
return bookmarkCount + " bookmarks";
}
}
});
But in any case I'd like to keep the actual helper simple and refer to external functions. For example:
Template.footerLinks.helpers({
updatedAt: 'wow',
formattedBookmarkCount: formatBookmarks(bookmarkCount)
});
.... somewhere else ....
function formatBookmarks(bookmarkCount) {
// call another function
return calcMessage(bookmarkCount);
}
function calcMessage(bookmarkCount) {
return bookmarkCount + " bookmarks";
}
To take it a step further, I'd like to access additional Meteor collections in the sub functions.
PARTIAL ANSWER
Thank you to #steph643 for pointing out the use of this. The following code now works:
Template.footerLinks.helpers({
updatedAt: 'wow',
formattedBookmarkCount: function() {
switch (this.bookmarkCount) {
case 0:
return "No bookmarks";
case 1:
return "1 bookmark";
default:
return this.bookmarkCount + " bookmarks";
}
},
But instead I would like to move this logic elsewhere, and possibly call it like this (which doesn't work):
Template.footerLinks.helpers({
updatedAt: 'wow',
formattedBookmarkCount: formatBookmarks()
}
Template.registerHelper('formatBookmarks', function() {
return this.bookmarkCount + " bookmarks";
}
This will return an error of
Uncaught ReferenceError: formatBookmarks is not defined
This is mostly a javascript thing. When you register a helper you're passing in an object that has a bunch of methods inside it & Meteor does its magic & puts those in a reactive context. If you want to use the function just inside helpers, then use a global helper like you kinda did, just rename the global to what you want & remove the local helper.
A second option is to create a global function & call it through the helper.
window.formatCount = function(count, singular, plural) {
switch (count) {
case 0:
return "No " + plural;
case 1:
return count + ' ' + singular;
default:
return count + ' ' + plural;
}
};
Template.registerHelper('formatBookmarksCount', function() {
return window.formatCount(this.bookmarkCount, 'bookmark', 'bookmarks')
}
Now you can use it anywhere on the client & you can consider namespacing a global object to avoid using window if you want (plenty of posts on SO regarding this).
Related
This is further information from a previous submission but I thought it would be clearer if I posted this separately.
A helper is returning a collection query:
Template.clientGrid.helpers({
'programs': function () {
var fullNameP = Session.get('clientName');
return Programs.find({FullName: fullNameP});
}
});
In the template it's printing out properties from 'programs'. For example:
...
{{#each programs}}
<p>{{formatCampYear CampYear}}: {{formatNotes Notes}}</p>
{{/each}}
....
Nothing special going on. So, if the FullName is Jane Doe, and she's got 6 documents in the programs collection, it will print the six properties in the template. But the page is getting caught in a while-loop inside Tracker (see line 449 the while-loop 'recompute all pending computations') after the properties finish printing. The CPU is tied up and prevents certain page operations. If any of you harder-core guys and gals have any clue as to what this means, perhaps I can sleuth out the problem. Here's a copy of the while loop itself (just in isolation):
// recompute all pending computations
while (pendingComputations.length) {
var comp = pendingComputations.shift();
comp._recompute();
if (comp._needsRecompute()) {
pendingComputations.unshift(comp);
}
if (! options.finishSynchronously && ++recomputedCount > 1000) {
finishedTry = true;
return;
}
}
EDIT: Here's the event map that setting the session. There doesn't seem to be anything suspicious. Since I'm pre-production, I'm not doing any updates to the collection. It's pretty much just static at this point.
Template.clientSearchButton.events({
'click #client-search-button': function(event) {
event.preventDefault();
var clientFullName = document.getElementById('full-name').value.toUpperCase();
Session.set('clientName', clientFullName);
mapAddress = Demographic.find({ "FullName": clientFullName }).map(function (a) { return (a.Address + " " + a.City + " " + a.State + " " + a.Country); });
Meteor.myFunctions.initialize();
}
});
I have a template helper function that converts my Mongo _id fields as a string:
Template.registerHelper('formatMongoId', function(data) {
return (data && data._str) || data;
});
I want to use it in a conditional statement within a template:
{{#if $eq box_group_id formatMongoId ../_id._str}}
....
{{/if}}
but this is not working - any ideas?
Note: the $eg bit is a comparison helper from a 3rd-party package.
Meteor doesn't make you follow a strict MVC, but you're essentially trying to cram a bunch of logic into the view layer. Instead, move all this logic into a single helper.
{{#if isEqual box_group_id ../_id._str}}
Template.foo.helpers({
isEqual: function (id1, id2) {
return idStr(id1) === idStr(id2);
}
});
function idStr(id) {
return id && id._str || id;
}
Now when you wake up a week from now, you'll be able to read your html & understand what's going on.
I'm trying to use a client side collection as a site configuration system. I insert documents representing my different pages, and the iron-router and navigation tabs all use them to determine what pages they are and what templates are represented by them. Each page uses a {{> contentTemplate}} inclusion helper to load it's relevant template.
It all works great, when the data has all loaded. When I restart the app on certain pages, the data hasn't loaded yet, and I receive the Exception from Deps recompute function: Error: Expected null or template in return value from inclusion function, found: undefined error.
Here's my javascript:
StoriesArray = [
{ category: 'teaching', contentTemplate: 'teachingHome', title: 'Teaching Home'},
...
];
Stories = new Meteor.Collection(null);
StoriesArray.forEach(function (story, index) {
story._id = index + '';
Stories.insert(story);
});
// in main.js
Template.teachingPost.contentTemplate = function() {
console.log(this);
console.log(this.contentTemplate);
return Template[this.contentTemplate];
};
// in router.js
this.route('teaching', {
layoutTemplate: 'teachingPost',
data: function() { return Stories.findOne({contentTemplate: 'teachingHome', category: 'teaching'}); }
});
The console logs in the contentTemplate helper above log twice, the first time as this:
Object {} main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:45
undefined main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:46
and the second time as this:
Object {category: "teaching", contentTemplate: "teachingHome", title: "Teaching Home"} main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:45
teachingHome main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:46
so the router is simply trying to load this data too early.
I've tried putting the StoriesArray loading process into different files all over my app, including lib, and even tried putting it into Meteor.startup, but it's always the same result.
The normal iron-router waitOn/subscription pattern doesn't really apply here, since this is a client side collection built with null, that has no server representation. I don't want this to have server representation, because this is static content that there's no need to go to my server for.
How do I ensure this information is done before continuing?
Untested, but per Iron Router's docs on waitOn:
Returning a subscription handle, or anything with a ready method from the waitOn function will add the handle to a wait list.
Also in general it's better to use find with data, rather than findOne, as find will return an empty cursor when the collection is empty as opposed to findOne returning undefined. So try this:
// in router.js
this.route('teaching', {
layoutTemplate: 'teachingPost',
data: function() {
return Stories.find({contentTemplate: 'teachingHome', category: 'teaching'});
},
waitOn: function() {
var handle = {};
handle.ready = function() {
if (Stories.find().count() !== 0)
return true;
else
return false;
}
return handle;
}
});
And adjust your Template.teachingPost.contentTemplate function to work with a cursor rather than an object.
General question: in Meteor, what's the best way to implement business logic that triggers whenever a model is updated -- e.g., for updating dependent fields or validations or...
Specific example: I'd like to add a "slug" field to Lists collection in the Meteor todos example. The slug needs to automatically update whenever a list's name is changed.
Here's what I've got... I'm observing every change to a list to see if its slug needs to be created/updated. This is in a shared models.js (runs server and client-side, to get the benefits of latency compensation):
// Lists -- {name: String}
Lists = new Meteor.Collection("lists");
var listsObserver = Lists.find().observe({
added: updateSlug,
changed: updateSlug
});
function updateSlug(doc, idx) {
var slug = (doc.name || '').replace(/\W+/g, '-').toLowerCase();
if (slug !== doc.slug) {
console.log("Updating slug for '" + doc.name + "' to " + slug);
Lists.update(doc._id, {$set: {slug: slug}});
}
}
(And as in the original todos example, server/publish.js publishes all of Lists.find() as "lists", and client/todos.js subscribes to that collection.)
The code above seems to work, but somehow doesn't look quite right to me. Questions:
Is observing the Lists collection like this a reasonable approach? It seems like
it could be inefficient -- any change to a Lists document will trigger this code.
Should I be doing a different (simulated) update client-side, or is it OK to let
this same Mongo/Minimongo update run on both?
Do I need to call listsObserver.stop() at some point to dispose the observer?
And if so, when?
(I'm just getting started with Meteor, so perhaps my biases from other environments are leaking through. The implied meta-question here is, am I even thinking about this problem in the right way?)
I would suggest using the Collection-Hooks package. It extends the collection operations with before and after hooks. This is better than having a lot of collection Observes or ObserveChanges, especially on the server where the overhead for collection observes can get very large.
This works on both the client and the server. If you implement it on the client you will get the benefit of updating the local collection (latency compensation) and the change will be pushed to the server so no need to do it again.
You also get the benefit of only doing one MongoDB operation instead of two or more like you would with observes or observeChanges.
You might use it like so:
var beforeInsertSlug = function(userId, doc) {
var slug = (doc.name || '').replace(/\W+/g, '-').toLowerCase();
if (slug !== doc.slug) {
console.log("Updating slug for '" + doc.name + "' to " + slug);
doc.slug = slug;
}
};
var beforeUpdateSlug = function(userId, doc, fieldNames, modifier, options){
if(modifier && modifier.$set && modifier.$set.doc && _.isString(modifier.$set.doc.name)){
var slug = (modifier.$set.doc.name || '').replace(/\W+/g, '-').toLowerCase();
if (slug !== doc.slug) {
console.log("Updating slug for '" + modifier.$set.doc.name + "' to " + slug);
modifier.$set.doc.slug = slug;
}
}
};
Lists.before.insert(beforeInsertSlug);
Lists.before.update(beforeUpdateSlug);
You can find the package here: https://atmospherejs.com/matb33/collection-hooks
I did a similar thing in server code. Basically put this code in Meteor.methods(), along with any other checks and updates you want making to the Lists Collection.
Although the code below looks a bit messy, and certainly hard to understand with the line starting with var slug:
Meteor.methods({
myupdate: function (doc) {
var slug = (doc.name || '').replace(/\W+/g, '-').toLowerCase();
if (slug !== doc.slug) {
console.log("Updating slug for '" + doc.name + "' to " + slug);
Lists.update(doc._id, {$set: {slug: slug}});
}
}
});
One way to implement this is to define a custom template function and trigger it in the template that is changing. For example:
In client.js
Template.myTemplate.custom_function_to_update = function() {
// do my update code. i.e. MyCollections.Update(...);
}
In the html file with the template
<template name="myTemplate">
<!-- Normal template code -->
{{ custom_function_to_update }}
</template>
and every time the template "myTemplate" updates, it will call your method.
I've been scratching my head as to why this code will work some of the time, but not all (or at least most of the time). I've found that it actually does run displaying the correct content in the browser some of the time, but strangely there will be days when I'll come back to the same code, run the server (as per normal) and upon loading the page will receive an error in the console: TypeError: 'undefined' is not an object (evaluating 'Session.get('x').html')
(When I receive that error there will be times where the next line in the console will read Error - referring to the err object, and other times when it will read Object - referring the data object!?).
I'm obviously missing something about Session variables in Meteor and must be misusing them? I'm hoping someone with experience can point me in the right direction.
Thanks, in advance for any help!
Here's my dummy code:
/client/del.html
<head>
<title>del</title>
</head>
<body>
{{> hello}}
</body>
<template name="hello">
Hello World!
<div class="helloButton">{{{greeting}}}</div>
</template>
My client-side javascript file is:
/client/del.js
Meteor.call('foo', 300, function(err, data) {
err ? console.log(err) : console.log(data);
Session.set('x', data);
});
Template.hello.events = {
'click div.helloButton' : function(evt) {
if ( Session.get('x').answer.toString() === evt.target.innerHTML ) {
console.log('yay!');
}
}
};
Template.hello.greeting = function() {
return Session.get('x').html;
};
And my server-side javascript is:
/server/svr.js
Meteor.methods({
doubled: function(num) {
return num * 2;
},
foo: function(lmt) {
var count = lmt,
result = {};
for ( var i = 0; i < lmt; i++ ) {
count++;
}
count = Meteor.call('doubled', count);
result.html = "<em>" + count + "</em>";
result.answer = count;
return result;
}
});
I think it's just that the session variable won't be set yet when the client first starts up. So Session.get('x') will return undefined until your method call (foo) returns, which almost certainly won't happen before the template first draws.
However after that it will be in the session, so things will probably behave right once you refresh.
The answer is to just check if it's undefined before trying to access the variable. For example:
Template.hello.greeting = function() {
if (Session.get('x')) return Session.get('x').html;
};
One of the seven principles of Meteor is:
Latency Compensation. On the client, use prefetching and model simulation to make it look like you have a zero-latency connection to the database.
Because there is latency, your client will first attempt to draw the lay-out according to the data it has at the moment your client connects. Then it will do the call and then it will update according to the call. Sometimes the call might be able to respond fast enough to be drawn at the same time.
As now there is a chance for the variable to not be set, it would throw an exception in that occasion and thus break down execution (as the functions in the call stack will not continue to run).
There are two possible solutions to this:
Check that the variable is set when using it.
return Session.get('x') ? Session.get('x').html : '';
Make sure the variable has an initial value by setting it at the top of the script.
Session.set('x', { html = '', answer = ''});
Another approach would be to add the templates once the call responds.
Meteor.call('foo', 300, function(err, data) {
Session.set('x', data);
$('#page').html(Meteor.ui.render(function() {
return Template.someName();
}));
});