How to DRY up Template Helpers in meteor? - meteor

My Template helper has duplicated code:
Template.foodMenu.helpers({
breakfast: function() {
var breakfastItems = EatingTimes.find(// query for breakfast items);
// function to sort breakfastItems in here (code duplication)
},
lunch: function() {
var lunchItems = EatingTimes.find(// query for lunch items);
// function to sort lunchItems in here (code duplication)
},
dinner: function() {
var dinnerItems = EatingTimes.find(// query for dinner items);
// function to sort breakfastItems in here (code duplication)
}
);
I would like to DRY it up:
Template.foodMenu.helpers({
breakfast: function() {
var breakfastItems = EatingTimes.find(// query for breakfast items);
sortFoodItems(breakfastItems);
},
lunch: function() {
var lunchItems = EatingTimes.find(// query for lunch items);
sortFoodItems(lunchItems);
},
dinner: function() {
var dinnerItems = EatingTimes.find(// query for dinner items);
sortFoodItems(dinnerItems);
}
);
Where do I place this function so I can DRY up? How do I name space it so I can call it properly? I am using Iron Router if that makes a difference.
var sortFoodItems = function (foodItems) {
// code to sort out and return foodItems to particular method that calls it
};

Just define you function before helpers in the same file
var sortFoodItems = function (foodItems) {
// code to sort out and return foodItems to particular method that calls it
};
Template.foodMenu.helpers({
breakfast: function() {
var breakfastItems = EatingTimes.find(/* query for breakfast items */);
sortFoodItems(breakfastItems);
},
lunch: function() {
var lunchItems = EatingTimes.find(/* query for lunch items */);
sortFoodItems(lunchItems);
},
dinner: function() {
var dinnerItems = EatingTimes.find(/* query for dinner items */);
sortFoodItems(dinnerItems);
}
});
If you want to use sortFoodItems function in multiple files, create folder with name lib and put the function in file functions.js without var keyword to make it global. For example:
//lib/functions.js
sortFoodItems = function (foodItems) {
// code to sort out and return foodItems to particular method that calls it
};
You need to understand how Meteor reads your project directories. Read more about structuring your application in Meteor docs.

You could also create a global helper for this using Template.registerHelper(name, function) which would look like:
Template.registerHelper('menuItems', function(eatingTime, sortCriteria) {
//do some checking of your arguments
eatingTime = eatingTime || '/* your default eating time */';
sortCriteria = sortCriteria || {/* your default sort criteria */};
check(eatingTime, String);
check(sortCriteria, Object);
//find and sort your items in one mongo query or you could do separate find and sort if you want
menuItems = EatingTimes.find({time: eatingTime}, sortCriteria);
return menuItems;
});
This would be the most meteoric way of DRYing up your code.
and then in your template, you could call it as:
{{#each menuItems 'time args' 'sort arg'}}

Related

Meteor publications/subscriptions not working as expected

I have two publications.
The first pub implements a search. This search in particular.
/* publications.js */
Meteor.publish('patients.appointments.search', function (search) {
check(search, Match.OneOf(String, null, undefined));
var query = {},
projection = {
limit: 10,
sort: { 'profile.surname': 1 } };
if (search) {
var regex = new RegExp( search, 'i' );
query = {
$or: [
{'profile.first_name': regex},
{'profile.middle_name': regex},
{'profile.surname': regex}
]
};
projection.limit = 20;
}
return Patients.find(query, projection);
});
The second one basically returns some fields
/* publications.js */
Meteor.publish('patients.appointments', function () {
return Patients.find({}, {fields: {'profile.first_name': 1,
'profile.middle_name': 1,
'profile.surname': 1});
});
I've subscribed to each publication like so:
/* appointments.js */
Template.appointmentNewPatientSearch.onCreated(function () {
var template = Template.instance();
template.searchQuery = new ReactiveVar();
template.searching = new ReactiveVar(false);
template.autorun(function () {
template.subscribe('patients.appointments.search', template.searchQuery.get(), function () {
setTimeout(function () {
template.searching.set(false);
}, 300);
});
});
});
Template.appointmentNewPatientName.onCreated(function () {
this.subscribe('patients.appointments');
});
So here's my problem: When I use the second subscription (to appointments.patients), the first one doesn't work. When I comment the second subscription, the first one works again. I'm not sure what I'm doing wrong here.
The issue here is you have two sets of publications for the same Collection. So when you refer to the collection in client there is now way to specify which one of the publication it has to refer too.
What you can do is, publish all data collectively i.e. all fields you are going to need and then use code on client to perform queries on them.
Alternatively, the better approach will be to have two templates. A descriptive code:
<template name="template1">
//Code here
{{> template2}} //include template 2 here
</template>
<template name="template2">
//Code for template 2
</template>
Now, subscribe for one publication to template one and do the stuff there. Subscribe to second publication to template 2.
In the main template (template1) include template2 in it using the handlebar syntax {{> template2}}

Reactive cursor without updating UI for added record

I am trying to make a newsfeed similar to twitter, where new records are not added to the UI (a button appears with new records count), but updates, change reactively the UI.
I have a collection called NewsItems and I a use a basic reactive cursor (NewsItems.find({})) for my feed. UI is a Blaze template with a each loop.
Subscription is done on a route level (iron router).
Any idea how to implement this kind of behavior using meteor reactivity ?
Thanks,
The trick is to have one more attribute on the NewsItem Collection Say show which is a boolean. NewsItem should have default value of show as false
The Each Loop Should display only Feeds with show == true and button should show the count of all the items with show == false
On Button click update all the elements in the Collection with show == false to show = true
this will make sure that all your feeds are shown .
As and when a new feed comes the Button count will also increase reactively .
Hope this Helps
The idea is to update the local collection (yourCollectionArticles._collection): all articles are {show: false} by default except the first data list (in order not to have a white page).
You detect first collection load using :
Meteor.subscribe("articles", {
onReady: function () {
articlesReady = true;
}
});
Then you observe new added data using
newsItems = NewsItems.find({})
newsItems.observeChanges({
addedBefore: (id, article, before)=> {
if (articlesReady) {
article.show = false;
NewsItems._collection.update({_id: id}, article);
}
else {
article.show = true;
NewsItems._collection.update({_id: id}, article);
}
}
});
Here is a working example: https://gist.github.com/mounibec/9bc90953eb9f3e04a2b3.
Finally I managed it using a session variable for the current date /time:
Template.newsFeed.onCreated(function () {
var tpl = this;
tpl.loaded = new ReactiveVar(0);
tpl.limit = new ReactiveVar(30);
Session.set('newsfeedTime', new Date());
tpl.autorun(function () {
var limit = tpl.limit.get();
var time = Session.get('newsfeedTime');
var subscription = tpl.subscribe('lazyload-newsfeed', time, limit);
var subscriptionCount = tpl.subscribe('news-count', time);
if (subscription.ready()) {
tpl.loaded.set(limit);
}
});
tpl.news = function() {
return NewsItems.find({creationTime: {$lt: Session.get('newsfeedTime')}},
{sort: {relevancy: -1 }},
{limit: tpl.loaded.get()});
},
tpl.countRecent = function() {
return Counts.get('recentCount');
},
tpl.displayCount = function() {
return Counts.get('displayCount');
}
});
Template.newsFeed.events({
'click .load-new': function (evt, tpl) {
evt.preventDefault();
var time = new Date();
var limit = tpl.limit.get();
var countNewsToAdd = tpl.countRecent();
limit += countNewsToAdd;
tpl.limit.set(limit);
Session.set('newsfeedTime', new Date());
}
});

How to show documents from multiple remote publication in the template?

I wish to use Meteor to subscribe a few remote publication via DDP. Then show the documents in one template. Here is what I did:
Posts = {};
var lists = [
{server: "localhost:4000"},
{server: "localhost:5000"}
];
var startup = function () {
_.each(lists, function (list) {
var connection = DDP.connect(`http://${list.server}`);
Posts[`${list.server}`] = new Mongo.Collection('posts', {connection: connection});
connection.subscribe("allPosts");
});
}
startup();
This file is at client folder. Every startup, in this example, at browser I have two client collections Posts["localhost:4000"] and Posts["localhost:5000"], both are same schema. I know this format (Collection[server]) is ugly, please tell me if there is a better way.
Is there a way to show these client collections in the same template with reactive. Like this:
Template.registerHelper("posts", function () {
return Posts.find({}, {sort: {createdAt: -1}});
});
I think Connected Client is a big part of the Meteor. There should be a best practice to solve this problem, right?
Solved.
Connect to multiple servers via DDP, then observe their collections reactive via cursor.observeChanges.
Posts = {};
PostsHandle = {};
// LocalPosts is a local collection lived at browser.
LocalPosts = new Mongo.Collection(null); // null means local
// userId is generated by another Meteor app.
var lists = [
{server: "localhost:4000", userId: [
"hocm8Cd3SjztwtiBr",
"492WZqeqCxrDqfG5u"
]},
{server: "localhost:5000", userId: [
"X3oicwXho45xzmyc6",
"iZY4CdELFN9eQv5sa"
]}
];
var connect = function () {
_.each(lists, function (list) {
console.log("connect:", list.server, list.userId);
var connection = DDP.connect(`http://${list.server}`);
Posts[`${list.server}`] = new Mongo.Collection('posts', {connection: connection}); // 'posts' should be same with remote collection name.
PostsHandle[`${list.server}`] = connection.subscribe("posts", list.userId);
});
};
var observe = function () {
_.each(PostsHandle, function (handle, server) {
Tracker.autorun(function () {
if (handle.ready()) {
console.log(server, handle.ready());
// learn from http://docs.meteor.com/#/full/observe_changes
// thank you cursor.observeChanges
var cursor = Posts[server].find();
var cursorHandle = cursor.observeChanges({
added: function (id, post) {
console.log("added:", id, post);
piece._id = id; // sync post's _id
LocalPosts.insert(post);
},
removed: function (id) {
console.log("removed:", id);
LocalPosts.remove(id);
}
});
}
})
});
}
Template.posts.onCreated(function () {
connect(); // template level subscriptions
});
Template.posts.helpers({
posts: function () {
observe();
return LocalPosts.find({}, {sort: {createdAt: -1}}); // sort reactive
}
});

Meteor - How can I pass data between helpers and events for a template?

I'm a bit new to Meteor and something I'm having trouble with is reactive data -- particularly in instances where I need to change the data shown based on a mouse or keyboard event. Doing this kind of stuff the normal js way seems to give me trouble in meteor since everything I change gets re-rendered and reset constantly.
So, I thought I'd see if this would be a case in which I could use Meteor's Deps object, however I can't quite grasp it. Here's the code I'm using:
(function(){
var tenants = [];
var selectedTenant = 0;
var tenantsDep = new Deps.Dependency;
Template.tenantsBlock.tenantsList = function()
{
tenants = [];
var property = $properties.findOne({userId: Meteor.userId(), propertyId: Session.get('property')});
var tenancies = _Utils.resolveTenancies(property, true, null, true);
for(var i = 0; i < tenancies.length; i++)
{
if(tenancies[i].tenancyId == Session.get('tenancy'))
{
tenants = tenants.concat(tenancies[i].otherTenants, tenancies[i].primaryTenant);
}
}
tenants[selectedTenant].selected = 'Selected';
tenantsDep.changed();
return tenants;
};
Template.tenantsBlock.onlyOneTenant = function()
{
tenantsDep.depend();
return tenants.length > 1 ? '' : 'OneChild';
};
Template.tenantsBlock.phoneNumber = function()
{
tenantsDep.depend();
for(var i = 0; i < tenants[selectedTenant].details.length; i++)
if(_Utils.getDynamicContactIconClass(tenants[selectedTenant].details[i].key) == 'Phone')
return tenants[selectedTenant].details[i].value;
return null;
};
Template.tenantsBlock.emailAddress = function()
{
tenantsDep.depend();
for(var i = 0; i < tenants[selectedTenant].details.length; i++)
if(_Utils.getDynamicContactIconClass(tenants[selectedTenant].details[i].key) == 'Email')
return tenants[selectedTenant].details[i].value;
return null;
};
Template.tenantsBlock.addedDate = function()
{
tenantsDep.depend();
return _Utils.timeToDateString(tenants[selectedTenant].created);
};
Template.tenantsBlock.events({
'click .Name': function(e, template)
{
tenantsDep.depend();
var _this = e.currentTarget;
var tenantName = _this.innerHTML;
$(_this).addClass('Selected');
$(_this).siblings().removeClass('Selected');
for(var i = 0; i < tenants.length; i++)
{
if(tenants[i].name == tenantName)
tenants[i].selected = "Selected";
else
tenants[i].selected = '';
}
}
})
})();
^This seemed to be what they were getting at in the meteor documentation (http://docs.meteor.com/#deps_dependency) for dependency.changed() and dependency.depend(), but all this does is give me an infinite loop.
So can I modify the way I declare deps to get this to make data reactive? Is there a better way to do this all together?
UPDATE:
Although I was skeptical to do so, I've been inclined to try to use Session.set/Session.get in a localized way. So, the next time I have to do this, I'll just do
Session.set('tenantsBlock' {tenants: [], selectedTenant: 0});
and then just access this variable from within helpers and event maps related to Template.tenantsBlock. That way they all have real time access to the data and they all get re-run when the data changes. Here's what I converted this script into (sorry these are both so large):
(function()
{
Template.tenantsBlock.created = Template.tenantsBlock.destroyed =function()
{
_Utils.setSession('tenantsBlock', {
tenants: [],
selectedTenant: 0
})
};
Template.tenantsBlock.tenantsList = function()
{
var localContext = Session.get('tenantsBlock');
localContext.tenants = [];
var property = $properties.findOne({userId: Meteor.userId(), propertyId: Session.get('property')});
var tenancies = _Utils.resolveTenancies(property, true, null, true);
for(var i = 0; i < tenancies.length; i++)
{
if(tenancies[i].tenancyId == Session.get('tenancy'))
{
localContext.tenants = localContext.tenants.concat(tenancies[i].otherTenants, tenancies[i].primaryTenant);
break;
}
}
localContext.tenants[localContext.selectedTenant].selected = 'Selected';
Session.set('tenantsBlock', localContext);
return localContext.tenants;
};
Template.tenantsBlock.onlyOneTenant = function()
{
var localContext = Session.get('tenantsBlock');
return localContext.tenants.length > 1 ? '' : 'OneChild';
};
Template.tenantsBlock.phoneNumber = function()
{
var localContext = Session.get('tenantsBlock');
for(var i = 0; i < localContext.tenants[localContext.selectedTenant].details.length; i++)
if(_Utils.getDynamicContactIconClass(localContext.tenants[localContext.selectedTenant].details[i].key) == 'Phone')
return localContext.tenants[localContext.selectedTenant].details[i].value;
return null;
};
Template.tenantsBlock.emailAddress = function()
{
var localContext = Session.get('tenantsBlock');
var selectedTenantDetails = localContext.tenants[localContext.selectedTenant].details;
for(var i = 0; i < selectedTenantDetails.length; i++)
if(_Utils.getDynamicContactIconClass(selectedTenantDetails[i].key) == 'Mail')
return selectedTenantDetails[i].value;
return null;
};
Template.tenantsBlock.addedDate = function()
{
var localContext = Session.get('tenantsBlock');
return _Utils.timeToDateString(localContext.tenants[localContext.selectedTenant].created);
};
Template.tenantsBlock.events({
'click .Name': function(e, template)
{
var localContext = Session.get('tenantsBlock');
var _this = e.currentTarget;
var tenantName = _this.innerHTML;
for(var i = 0; i < localContext.tenants.length; i++)
{
if(localContext.tenants[i].name == tenantName)
{
localContext.tenants[i].selected = 'Selected';
localContext.selectedTenant = i;
}
else
{
localContext.tenants[i].selected = '';
}
}
Session.set('tenantsBlock', localContext);
}
})
})();
You'll have to overcome the old-school way of doing it :) Meteor is a lot simpler than you think. A good rule of thumb is that if you're using jQuery to manipulate any DOM elements, you're probably doing it wrong. Additionally, if you're accessing any data without using the collection API, you'd better have good reason to do so.
In your case, you don't need to code up any manual dependencies at all. Manual dependencies are rarely needed in most Meteor applications.
The first thing you need to do is put all your tenants inside a Meteor.Collection, which will make them easier to work with.
Tenants = new Meteor.Collection("tenants");
Your tenantsBlock template should look something like this (modulo some different html elements):
<template name="tenantsBlock">
<ol>
{{#each tenants}}
<li class="name {{selected}}">
<span>Primary Tenant: {{primaryTenant}}</span>
<span>Other Tenants: {{otherTenants}}</span>
<span>Phone Number: {{phoneNumber}}</span>
<span>Email Address: {{emailAddress}}</span>
<span>Added Date: {{addedDate}}</span>
</li>
{{/each}}
</ol>
</template>
Each document in Tenants should look something like the following:
{
primaryTenant: "Joe Blow",
otherTenants: "Mickey Mouse, Minnie Mouse",
phoneNumber: "555-234-5623",
emailAddress: "joe.blow#foo.com",
addedDate: "2005-10-30T10:45Z"
}
Then, all the code you would need is just for the selection/deselection, and you can delete everything else:
Template.tenantsBlock.tenants = function() {
return Tenants.find();
};
Template.tenantsBlock.selected = function() {
return Session.equals("selectedTenant", this._id);
};
Template.tenantsBlock.events({
'click .name': function(e) {
Session.set("selectedTenant", this._id);
}
});
Once again, I reiterate that you should never be doing DOM manipulations with Javascript when using Meteor. You just update your data and your templates will reactively update if everything is done correctly. Declare how you want your data to look, then change the data and watch the magic.
Meteor has really evolved since I posted this back in 2013. I thought
I should post a modern, superior method.
For a while now you've been able to create a ReactiveVar and now you can append those directly to templates. A ReactiveVar, similar to Session, is a reactive data store. ReactiveVar, however, holds only a single value (of any type).
You can add ReactiveVar to the client side of your project by running this in your terminal from your app's root directory:
$meteor add reactive-var
This javascript shows how you can pass the variable between the template's onCreated, onRendered, onDestroyed, events and helpers.
Template.myTemplate.onCreated = function() {
// Appends a reactive variable to the template instance
this.reactiveData = new ReactiveVar('Default Value');
};
Template.myTemplate.events({
'click .someButton': (e, template) => {
// Changes the value of the reactive variable for only this template instance
template.reactiveData.set('New Value');
},
});
Template.myTemplate.helpers({
theData: () => {
// Automatically updates view when reactive variable changes
return Template.instance().reactiveData.get();
},
});
This is superior for a few reasons:
It scopes the variable only to a single template instance. Particularly useful in cases where you might have a dozen instances of a template on a page, all requiring independent states.
It goes away when the template goes away. Using ReactiveVar or Session variables you will have to clear the variable when the template is destroyed (if it is even destroyed predictably).
It's just cleaner code.
Bonus Points: See ReactiveDict for cases in which you have many instances of a template on a page at once, but need to manage a handful of reactive variables and have those variables persist during the session.

How to implement Full Text Search in Meteor/Telescope

I have attempted implementing search in Telescope using pure javascript, since it looks like FTS is a while off for Meteor to implement and I couldn't get 2.4 playing nicely with Meteor yet.
I'm using the existing pagination model that is already implemented in Telescope to display the Top/New/Best posts, plus a Session variable for the search keyword that is set in the Router when you navigate to e.g. /search/foobar.
However, it doesn't quite seem to be working; when I have, say, 100 posts, the regular paginated subscription only comes back with 25 of these and my search results only show the posts in the first 25.
I've been banging my head against a wall for days trying to debug this one: sometimes it works, sometimes it doesn't!
Here's the code (I've included all additional search code for reference):
app.js:
var resultsPostsSubscription = function() {
var handle = paginatedSearchSubscription( 10, 'searchResults' );
handle.fetch = function() {
return limitDocuments( searchPosts( Session.get( 'keyword' ) ), handle.loaded() );
};
return handle;
};
var resultsPostsHandle = resultsPostsSubscription();
paginated_sub.js:
I duplicated the existing paginatedSubscription because I can't pass a Session var in as an arg; it needs to be dynamic. I'll probably refactor later.
paginatedSearchSubscription = function (perPage/*, name, arguments */) {
var handle = new PaginatedSubscriptionHandle(perPage);
var args = Array.prototype.slice.call(arguments, 1);
Meteor.autosubscribe(function() {
var subHandle = Meteor.subscribe.apply(this, args.concat([
Session.get( 'keyword' ), handle.limit(), function() { handle.done(); }
]));
handle.stop = subHandle.stop;
});
return handle;
}
search.js: (new file, in /common directory)
// get all posts where headline, categories, tags or body are LIKE %keyword%
searchPosts = function( keyword ) {
var query = new RegExp( keyword, 'i' );
var results = Posts.find( { $or: [ { 'headline': query }, { 'categories': query }, { 'tags': query }, { 'body': query } ] } );
return results;
};
publish.js:
Meteor.publish( 'searchResults', searchPosts );
posts_list.html:
<template name="posts_results">
{{> posts_list resultsPostsHandle}}
</template>
posts_list.js:
Template.posts_results.resultsPostsHandle = function() {
return resultsPostsHandle;
};
router.js:
there's a search bar in the nav that redirects to here
posts_results = function( keyword ) {
Session.set( 'keyword' , keyword );
return 'posts_results';
};
Meteor.Router.add({
...
'/search/:keyword':posts_results,
...
})
Any help would be greatly appreciated!
A little late but here is a full write up on how to implement full text search in meteor.
"The simplest way without editing any Meteor code is to use your own mongodb."

Resources