Using a template function in a conditional statement - meteor

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.

Related

How to call a function in data-binding when lots of parameters are involved

At the moment I have this in my template:
WAY 1
<template is="dom-if" if="{{_showCancel(userData.generic.id,info.userId,info._children.gigId.userId,info.accepted,info.cancelled,info.paymentTerms)}}">
And then this in my element:
_showCancel: function(viewerUserId,offerUserId,gigUserId, accepted, cancel, paymentTerms) {
// Offer needs to be accepted and NOT cancelled
if (!info.accepted || info.cancelled) return false;
// Gig owner can ALWAYS cancel an offer
if (viewerUserId == gigUserId ) return true;
// Offer maker can only cancel if the gig involved pre-payment
if (viewerUserId == offerUserId && paymentTerms != 'on-day') return true;
return false;
},
Do I have just too many parameters to this function?
WAY 2
Should I just have something like this instead:
<template is="dom-if" if="{{_showCancel(userData, info)}}">
WAY 3
Although I would want to check if their sub-properties change too... so I would need:
<template is="dom-if" if="{{_showCancel(userData, info, userData.*, info.*)}}">
WAY 4
But then again I probably should just look for the properties and use the value property like so:
<template is="dom-if" if="{{_showCancel(userData.*, info.*)}}">
And then the function would be:
_showCancel: function(userDataObs, infoObs) {
var userData = userDataObs.value;
var info = infoObs.value;
if( !userData || !info) return;
...
Questions:
Do you see any fundamental mistakes with ways 1 to 4?
Is WAY 1 really the best way to go about it? (it feels like it right now)
Is WAY 3 an acceptable pattern?
ADDENDUM
What would I do in cases where I have:
<paper-button
raised
hidden$="{{!_showCancel(item.id,userData.config.usersCategories,userData.config.userCategories,userData.config.usersCategories.*)}}"
>
Cancel
</paper-button>
(NOTE: usersCategories is an Array)
And _showCancel being:
_showCancel: function(categoryId, userCategories) {
for (var k in usersCategories) {
var $ucat = usersCategories[k];
if ($ucat.categoryId == categoryId) {
if ($ucat.status == 'applied' || $ucat.status == 'granted') return true;
}
}
return false;
},
The point being: I want both easy access to usersCategories, but don't want to get the value out of the "strange" array modifiers etc. So, is this a good pattern?
The "Way 1" is the right one. But, you should only reference the variables that you need, and you should always use them as they are defined in your function header.
For example, you use:
{{_showCancel(userData.generic.id,info.userId,info._children.gigId.userId,info.accepted,info.cancelled,info.paymentTerms)}}
with the following function header:
_showCancel: function(viewerUserId,offerUserId,gigUserId, accepted, cancel, paymentTerms).
But then inside the function, you reference info.accepted and info.cancelled, whereas you should used accepted and cancelled.
This is because inside the function, the referenced values will always be up-to-date, whereas referencing variables via this.<variable-name> can sometimes contain older values.
In order for my answer to be complete, I will also explain certain "problems" with other ways.
Way 2: Here, you only reference the Object as a whole. This won't trigger the call via subproperty change, so it won't work as desired.
Way 3 and Way 4 are similar, and both are overkills. With the object.* notation, you listen to all subproperty changes, which you most likely don't want.
tl;dr
Go with "Way 1" and make things simpler by using computed properties.
To do this, change:
<template is="dom-if" if="{{_showCancel(userData.generic.id,info.userId,info._children.gigId.userId,info.accepted,info.cancelled,info.paymentTerms)}}">
To:
<template is="dom-if" if="{{isCanceled}}">
And add the following computed property to the Polymer element:
isCanceled: {
type: Boolean,
computed: '_showCancel(userData.generic.id,info.userId,info._children.gigId.userId,info.accepted,info.cancelled,info.paymentTerms)'
}
You already have _showCancel defined, so this is actually it. The code should work the same as your "Way 1" example, only the dom-if is cleaner. This is especially useful if you re-use the condition on multiple occurences.
If you have any questions, don't hesitate do add a comment about it.

How to use angular-meteor helpers and Meteor.methods together

I use a bunch of helper methods in my project. Some of them require to load the whole collection into the client due the restriction of the api on client side (the distinct function!). I googled the problem and found Meteor.methods as solution.
Can I use helpers (like this.helpers) into Meteor methods? Or how should I dynamically update my data in the frontend?
Can someone give me an example?
Additional information:
class View2 {
constructor($interval, $scope, $reactive) {
'ngInject';
$reactive(this).attach($scope);
this.helpers({
getOrderNumber(){
this.tempVar = Kafkadata.find().fetch();
this.tempVar2 = _.pluck(this.tempVar, 'orderNumber');
this.tempVar3 = _.uniq(tempVar2, false);
return this.tempVar3;
},
});
}
This is an example for a helpers query. Currently, this code runs client-side. I get ALL orders(tempvar) and then remove ALL data except the ordernumbers(tempvar2). At the end I remove all multiple ordernumbers. ordernumber is not an unique value. Here is an example from one of the collections:
{"orderNumber":"f2a3ed95-fcc3-4da0-9b3f-32cf5ed087f8","value":12480,"booleanValue":false,"intValue":12480,"doubleValue":0,"status":"GOOD","itemName":"MILLING_SPEED","timestamp":1479145734448,"_id":{"_str":"5824f4bc7ff3f0199861f11d"}}
I want to use functions like db.collection.distinct(). But they only work server-side. I think I must use Meteor.methods()to make this thing server-side. But what about this helpers function? How do they work on Meteor.methods()?
EDIT2:
my test:
client-side:
folder:myProject/imports/ui/view1
class View1 {
constructor($interval, $scope, $reactive) {
'ngInject';
$reactive(this).attach($scope);
this.helpers({
// some code
getTestData(){
Meteor.call('allTestData',function(error, result){
if(error){
console.log("error");
}else{
return result;
}
});
}
}); //end of contructor
// this is my testfunction, which is bound to a button!
testFunction(){
Meteor.call('allTestData',function(error, result){
if(error){
alert('Error');
}else{
console.log(result);
}
});
}
on the server-side:
folder:myProject/server/main.js
Meteor.methods({
allTestData:()=>{
var results=Kafkadata.find().count();
console.log(results);
return results;
},
});
and this is my view1.html:
//some code
<md-button ng-click="view1.testFunction()"> It works!</md-button>
<h1>{{view1.getTestData}}</h1>
Why does the button work, but not the helper?
Even though .distinct is supported by Mongo, Meteor does not expose it, even on the server. You just have to use _.uniq as your example shows, but for performance reasons it's better if it runs on the server.
Below is an example of a helper that I use:
aweek: () => {
if (debug)
console.log("Querying weekly appointments for "+this.selectedWeek.format("Do MMMM"));
var weekApts = Appointments.find(
{start: {$gte: new Date(this.getReactively('this.selectedWeek').clone().day(1)),
$lt: new Date(this.getReactively('this.selectedWeek').clone().endOf('week'))},
elderid: Meteor.userId()
}).fetch();
return utils.services.getAWeek(weekApts,utils.data.elderTimeFormat);
},
Note the use of this.getReactively('this.selectedWeek') in the code... basically this tells Meteor to run this helper reactively, so if the value of this.selectedWeek changes, the helper will get re-run. So when I click on a week in the calendar and update the variable, it runs my helper again to get the data.
The utils.services.getAWeek() function does some calculation and formatting on the array of data that makes is easier to display.
If you create a Meteor Method to do processing, I would make it update a collection with its results, and then your helper on the client will update automatically. Best make the technology do the work for you :)

Exception in template helper: Error: Match error

I'm trying to perform a custom sort using a comparator function from within a template helper in Meteor.
Here is my template helper:
Template.move_list.helpers({
assets() {
return Assets.find({}, { sort: sortFunction });
}
});
And here is the comparator function:
const sortFunction = function (doc1, doc2) {
const barcodes = Session.get('barcodesArray');
if (barcodes.indexOf(doc1.barcode) === -1 || barcodes.indexOf(doc2.barcode) === -1) {
return 0;
}
let last = null;
_.each(barcodes, function (barcode) {
if (barcode === doc1.barcode) last = doc1.barcode;
if (barcode === doc2.barcode) last = doc2.barcode;
});
return last === doc1.barcode ? 1 : -1;
}
Error
When the page loads, the following error is returned:
Exception in template helper: Error: Match error: Failed Match.OneOf, Match.Maybe or Match.Optional validation
I put a breakpoint in chrome into the sortFunction, however the function was never entered and the breakpoint never reached.
Of course, the error is not throw when I remove sort.
References
This feature is not very well documented, however here is the relevant part of the docs:
For local collections you can pass a comparator function which receives two document objects, and returns -1 if the first document comes first in order, 1 if the second document comes first, or 0 if neither document comes before the other. This is a Minimongo extension to MongoDB.
And the commit by mitar adding the functionality, with example code from the test:
var sortFunction = function (doc1, doc2) {
return doc2.a - doc1.a;
};
c.find({}, {sort: sortFunction})
Can anyone make sense of this error?
Edit:
This issue should be resolved in Meteor >= v1.3.3.1.
Local collections (i.e, client-side and in-memory server-side collections) will allow to pass a function as the sort clause.
The error comes from the mongo package, where the spec does not allow sort to be a function.
#mitar changed LocalCollection in the minimongo package. LocalCollection is part of the Mongo.Collection object on the client (its _collection attribute), but queries are still checked according to the original mongo spec. I believe this to be a bug, as the spec was not updated to reflect the change.
To overcome this (in the meantime), either have the function accept a sub-field, such that the sort value is an object:
var sortFunction = function (x, y) {
return x - y;
};
c.find({}, {sort: {a: sortFunction}});
or use the c._collection.find() instead, which will work (as far as I can tell), except it will not apply any transformations defined for the collection.
var sortFunction = function (doc1, doc2) {
return doc2.a - doc1.a;
};
c._collection.find({}, {sort: sortFunction});

Calling external functions from Meteor Template helpers

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).

Meteor: issue with reactivity in the #each loop

Unfortunately, I am not allowed to post images, so that makes it a bit harder to show the issue at hand. So I will describe it here.
In my final Meteor on screen display, I have an invoice with line-items. The line-items and all work perfectly and show as expected.
What I wanted to do, was "break in" in the #each-loop after 4 line-items are printed on screen, and then do something else (calculate and print sub-totals). I can't get that to work, since the session var I created for that does not seem to reactively change on every line-item. Instead, it seems to hold the last value from the complete #each loop.
Note, that there is a listindex variable in there that perfectly re-actively builds up from 1, to 2,3, and 4 on each line-item that is within the #each loop. So, the listindex variable does work reactively within the #each loop.
At the same time, I am introducing a session var in there, that should be "true" on line-item number 4, and "false" in all other cases. On "true" it should print "onderbreking" right in there. However, the session var always gives "true" and (because of that) always prints "onderbreking". That's not how I intended it to work of course.
I do not understand why the listindex works, and the session var does not?
Here is my javascript on this. The template is called "layout6.html":
Template.layout6.helpers({
// Bereken de index in de #each loop
listIndex: function() {
currentIndex += 1;
if(currentIndex == 4) {
Session.set('sessionPageBreak1', true);
}
if(currentIndex != 4) {
Session.set('sessionPageBreak1', false);
}
return currentIndex;
},
});
Template.layout6.SessionPageBreak1 = function() {
// Voeg de waarde van de sessie variabele toe aan de template
var data = Session.get('sessionPageBreak1');
return data;
};
The session var is initially set here:
// Session var for layoutcounter
Session.setDefault('sessionPageBreak1', false);
Session.setDefault('sessionPageBreak2', false);
Session.setDefault('sessionPageBreak3', false);
var currentIndex = 0.0;
Finally, this is the html involved on layout6.html:
{{#each factuur6.lineItems}}
{{listIndex}}
{{SessionPageBreak1}}
{{#if SessionPageBreak1}}
<div><p>onderbreking</p></div>
{{/if}}
And of course, there is a {{/each}} statement as well, but there is a lot of code in between that is not very relevant for this issue, so I did not put it in here.
I hope I included everything, if not, you guys doubtlessly will tell me.
The question is: what am I doing wrong, so that the session var is not reactively changing within the #each loop?
This isn't really the right way to use Session variables. Session variables are for global application state, not for storing intermediate values through a single computation. The general rule is that templates and template helpers should not have side-effects (like setting Session variables).
I would take a different approach. Define a helper:
lineItemsWithIndex: function () {
return _.map(this.factuur6.lineItems, function (item, index) {
var newItem = _.clone(item);
newItem.index = index;
return newItem;
});
}
This function returns a copy of this.factuur6.lineItems, except each object has an index property added to it. For example, if the list of lineItems is [{name: "foo"}, {name: "bar"}, {name: "baz"}], we return [{name: "foo", index: 0}, {name: "bar", index: 1}, {name: "baz", index: 2}]. The original list is unchanged. If factuur6 is a helper rather than a field, then instead use _.map(Template.layout6.factuur6().lineItems ....
Then iterate over that in your template:
{{#each lineItemsWithIndex}}
{{index}}
{{pageBreak1}}
{{#if pageBreak1}}
<div><p>onderbreking</p></div>
{{/if}}
{{/each}}
Where pageBreak1 is a helper function () {return this.index == 3;} (3 rather than 4, since these indices are 0-based). Since we added the index to each object, it is now accessible in the helper.
This assumes that your line items don't already have a field called index.

Resources