PROBLEM: I want to parse the elements in a page from another website, glue resulting elements in an object and insert it in a Mongo collection. Before insertion i want to check if my Mongo yet has an identical object. If it does it shall exit the running functions, otherwise i want the script to start parsing the next target.
Example:
I have a function that connects to a webpage and returns its body content
It is parsed
When <a></a> elements are met, another callback is called in which all parsed elements are merged in one object and inserted in a collection
My code :
var Cheerio = Meteor.npmRequire('cheerio');
var lastUrl;
var exit = false;
Meteor.methods({
parsing:function(){
this.unblock();
request("https://example.com/", Meteor.bindEnvironment(function(error, response, body) {
if (!error && response.statusCode == 200) {
$ = Cheerio.load(body);
var k = 1;
$("div.content").each(function() {
var name = $...//parsing
var age = $....//parsing
var url = $...//parsing <a></a> elements
var r = request("https://example.com/"+url, Meteor.bindEnvironment(function(error, response, body) {
lastUrl = response.request.uri.href;// get the last routing link
var metadata = {
name: name,
age: age
url: lastUrl
};
var postExist;
postExist = Posts.findOne(metadata); // return undefined if doesnt exist, AND every time postExist = undefined ??
if (!postExist){
Posts.insert(metadata);// if post doesnt exist (every time go here ??)
}
else {
exit = true; // if exist
}
}));
if (exit === true) return false;
});
}
}));
}
});
Problem 1 : The problem is my function works every time, but it doesn't stop even if the object exists in my collection
Problem 2 : postExist is always undefined
EDIT : The execution must stop and wait until the second request's response.
var url = $...//parsing <a></a> elements
//STOP HERE AND WAIT !!
var r = request("https://example.com/"+url, Meteor.bindEnvironment(function(error, response, body) {
Looks like you want the second request to be synchronous and not asynchronous.
To achieve this, use a future
var Cheerio = Meteor.npmRequire('cheerio');
var Future = Meteor.npmRequire('fibers/future');
var lastUrl;
var exit = false;
Meteor.methods({
parsing:function(){
this.unblock();
request("https://example.com/", Meteor.bindEnvironment(function(error, response, body) {
if (!error && response.statusCode == 200) {
$ = Cheerio.load(body);
var k = 1;
$("div.content").each(function() {
var name = $...//parsing
var age = $....//parsing
var url = $...//parsing <a></a> elements
var fut = new Future();
var r = request("https://example.com/"+url, Meteor.bindEnvironment(function(error, response, body) {
lastUrl = response.request.uri.href;// get the last routing link
var metadata = {
name: name,
age: age
url: lastUrl
};
var postExist;
postExist = Posts.findOne(metadata); // return undefined if doesnt exist
if (!postExist) {
Posts.insert(metadata);// if post doesnt exist (every time go here ??)
fut.return(true);
} else {
fut.return(false);
}
}));
var status = fut.wait();
return status;
});
}
}));
}
});
You can use futures whenever you can't utilize callback functions (e.g. you want the user to wait on the result of a callback before presenting info).
Hopefully that helps,
Elliott
This is the opposite :
postExist = Posts.findOne(metadata); // return undefined if doesnt exist > you're right
if (!postExist){ //=if NOT undefined = if it EXISTS !
Posts.insert(metadata);
}else {
exit = true; // if undefined > if it DOES NOT EXIST !
}
You need to inverse the condition or the code inside
Related
I have been working on a end-to-end test using Webdriver I/O from Jasmine. One specific scenario has been giving me significant challenges.
I have a page with 5 links on it. The number of links actually challenges as the page is dynamic. I want to test the links to see if each links' title matches the title of the page that it links to. Due to the fact that the links are dynamically generated, I cannot just hard code tests for each link. So, I'm trying the following:
it('should match link titles to page titles', function(done) {
client = webdriverio.remote(settings.capabilities).init()
.url('http://www.example.com')
.elements('a').then(function(links) {
var mappings = [];
// For every link store the link title and corresponding page title
var results = [];
for (var i=0; i<links.value.length; i++) {
mappings.push({ linkTitle: links.value[0].title, pageTitle: '' });
results.push(client.click(links.value[i])
.getTitle().then(function(title, i) {
mappings[i].pageTitle = title;
});
);
}
// Once all promises have resolved, compared each link title to each corresponding page title
Promise.all(results).then(function() {
for (var i=0; i<mappings.length; i++) {
var mapping = mappings[i];
expect(mapping.linkTitle).toBe(mapping.pageTitle);
}
done();
});
});
;
});
I'm unable to even confirm if I'm getting the link title properly. I believe there is something I entirely misunderstand. I am not even getting each links title property. I'm definately not getting the corresponding page title. I think I'm lost in closure world here. Yet, I'm not sure.
UPDATE - NOV 24
I still have not figured this out. However, i believe it has something to do with the fact that Webdriver I/O uses the Q promise library. I came to this conclusion because the following test works:
it('should match link titles to page titles', function(done) {
var promise = new Promise(function(resolve, reject) {
setTimeout(function() { resolve(); }, 1000);
});
promise.then(function() {
var promises = [];
for (var i=0; i<3; i++) {
promises.push(
new Promise(function(resolve, reject) {
setTimeout(function() {
resolve();
}, 500);
})
);
}
Promise.all(promises).then(function() {
expect(true).toBe(true)
done();
});
});
However, the following does NOT work:
it('should match link titles to page titles', function(done) {
client = webdriverio.remote(settings.capabilities).init()
.url('http://www.example.com')
.elements('a').then(function(links) {
var mappings = [];
// For every link store the link title and corresponding page title
var results = [];
for (var i=0; i<links.value.length; i++) {
mappings.push({ linkTitle: links.value[0].title, pageTitle: '' });
results.push(client.click(links.value[i])
.getTitle().then(function(title, i) {
mappings[i].pageTitle = title;
});
);
}
// Once all promises have resolved, compared each link title to each corresponding page title
Q.all(results).then(function() {
for (var i=0; i<mappings.length; i++) {
var mapping = mappings[i];
expect(mapping.linkTitle).toBe(mapping.pageTitle);
}
done();
});
})
;
});
I'm not getting any exceptions. Yet, the code inside of Q.all does not seem to get executed. I'm not sure what to do here.
Reading the WebdriverIO manual, I feel like there are a few things wrong in your approach:
elements('a') returns WebElement JSON objects (https://code.google.com/p/selenium/wiki/JsonWireProtocol#WebElement_JSON_Object) NOT WebElements, so there is no title property thus linkTitle will always be undefined - http://webdriver.io/api/protocol/elements.html
Also, because it's a WebElement JSON object you cannot use it as client.click(..) input, which expects a selector string not an object - http://webdriver.io/api/action/click.html. To click a WebElement JSON Object client.elementIdClick(ID) instead which takes the ELEMENT property value of the WebElement JSON object.
When a client.elementIdClick is executed, the client will navigate to the page, trying to call client.elementIdClick in the next for loop cycle with next ID will fail, cause there is no such element as you moved away from the page. It will sound something like invalid element cache.....
So, I propose another solution for your task:
Find all elements as you did using elements('a')
Read href and title using client.elementIdAttribute(ID) for each of the elements and store in an object
Go through all of the objects, navigate to each of the href-s using client.url('href'), get the title of the page using .getTitle and compare it with the object.title.
The source I experimented with, not run by Jasmine, but should give an idea:
var client = webdriverio
.remote(options)
.init();
client
.url('https://www.google.com')
.elements('a')
.then(function (elements) {
var promises = [];
for (var i = 0; i < elements.value.length; i++) {
var elementId = elements.value[i].ELEMENT;
promises.push(
client
.elementIdAttribute(elementId, 'href')
.then(function (attributeRes) {
return client
.elementIdAttribute(elementId, 'title')
.then(function (titleRes) {
return {href: attributeRes.value, title: titleRes.value};
});
})
);
}
return Q
.all(promises)
.then(function (results) {
console.log(arguments);
var promises = [];
results.forEach(function (result) {
promises.push(
client
.url(result.href)
.getTitle()
.then(function (title) {
console.log('Title of ', result.href, 'is', title, 'but expected', result.title);
})
);
});
return Q.all(promises);
});
})
.then(function () {
client.end();
});
NOTE:
This fails to solve your problem, when the links trigger navigation with JavaScript event handlers not the href attributes.
This is my code when I insert multi record into my Collection first time it work well but when i insert second time it say duplicate _id, how to fix it.
//Save Journal Detail
$("#item-list tr").each(function (e) {
//debugger;
var yearMonth = $('#journalDate').val();
var date = moment(yearMonth).format("YYMM");
var prefix = curBranch + "-" + date;
var chartAccountId = $(this).find(".chart-account-id option:selected").val();
if (chartAccountId == "" || chartAccountId == null) {
return;
}
var journalDetailId = idGenerator.genWithPrefix(Acc.Collection.JournalDetail, prefix, 8);
var debit = parseFloat($(this).find(".debit").val());
var credit = parseFloat($(this).find(".credit").val());
if (debit > 0) {
Acc.Collection.JournalDetail.insert({
_id: journalDetailId,
journalId: doc._id,
chartAccountId: chartAccountId,
drCr: debit,
split: "Hello"
});
} else {
Acc.Collection.JournalDetail.insert({
_id: journalDetailId,
journalId: doc._id,
chartAccountId: chartAccountId,
drCr: -credit,
split: "Hello"
});
}
});
return doc;
The problem is likely to be that your $.each() iterations do not wait for your insert calls to finish, therefore the last id in the collection stays the same, and therefore idGenerator.genWithPrefix gives you the same id for multiple successive insert calls.
In the Meteor client, "insert never blocks". So you would have to wait on each insert call before getting the next id via idGenerator.genWithPrefix, or else your next generated id will end up being the same as the previous one you started to insert.
One easy solution could be to transfer that logic to the server (where insert calls are blocking) using a method call, by only giving it the list of documents you wish for it to insert.
On the client:
//Save Journal Detail
var listToInsert = [];
var yearMonth = $('#journalDate').val();
var date = moment(yearMonth).format("YYMM");
var prefix = curBranch + "-" + date;
$("#item-list tr").each(function (e) {
//debugger;
var chartAccountId = $(this).find(".chart-account-id option:selected").val();
if (chartAccountId == "" || chartAccountId == null) {
return;
}
var debit = parseFloat($(this).find(".debit").val());
var credit = parseFloat($(this).find(".credit").val());
listToInsert.push({
journalId: doc._id,
chartAccountId: chartAccountId,
drCr: debit > 0 ? debit : -credit, // simplified with a ternary operator
split: "Hello"
});
});
Meteor.call("insertEntries", listToInsert, prefix, function () {
//done!
});
return doc;
On the server:
Meteor.methods({
'insertEntries': function (listToInsert, prefix) {
for (var i = 0; i < listToInsert.length; i++) {
listToInsert[i]._id = idGenerator.genWithPrefix(Acc.Collection.JournalDetail, prefix, 8);
// Since we're on the server, this insert will be blocking
Acc.Collection.JournalDetail.insert(listToInsert[i]);
}
}
});
I am trying to publish forum replies to a specific thread, but I would like those reply documents to include extra information about the user that posted it.
I don't want to "save" that extra information on the reply itself but rather, publish an "improved" version of it.
I am doing something similar on client-side already with mycollection.find().map() and using the map function to embedded extra information on each of the returned documents, however, Meteor publish cannot seem to publish an array, only a Cursor, so the simple map function is off limits.
Is there a way to achieve this? Maybe a "map" function that returns a Cursor?
I am not using Meteor.methods so that I can have reactivity, because with them I could just return an array and use it as normal.
Here is an example of my code (that fails, but sends gives an idea of what I need):
Meteor.publish("forumthread", function(thread){
return forumReplies.find({thread: thread}).map(function(r){
// lets fill in additional data about each replies owner
var owner = Meteor.users.findOne({_id: r.owner});
if(!owner)
return; // no owner no reply..
if(!owner.forumStats){
owner.forumStats = {};
owner.forumStats.postCount = 0;
owner.forumStats.postLikes = 0;
owner.forumStats.title = "The Newbie";
owner.forumStats.tag = "Newbie";
Meteor.users.update({_id: owner._id}, {$set:{ forumStats:owner.forumStats }});
}
r.ownerid = owner._id;
r.ownerUsername = owner.username;
r.ownerPostCount = owner.forumStats.postCount;
r.ownerPostLikes = owner.forumStats.postLikes;
r.ownerTitle = owner.forumStats.title;
r.ownerTag = owner.forumStats.tag;
return r;
});
});
Thank you.
Ended up doing this (found out that Christian Fritz also suggested it):
Meteor.publish("serverforumthread", function(thread){
check(thread, String);
var replies = forumReplies.find({thread: thread});
var users = {};
replies.map(function(r){
users[r.owner] = r.owner;
});
var userids = _.map(users, function(value, key){ return value; });
var projectedFields = {_id:1, username:1, forumStats: 1, services: 0};
var usrs = Meteor.users.find({_id:{$in: userids}}, projectedFields);
var anyUpdateToUsers = false;
usrs.map(function(owner){
var changed = false;
if(!owner.username){
owner.username = owner.emails[0].address.split("#")[0];
changed = true;
}
//owner.forumStats = undefined;
if(!owner.forumStats){
owner.forumStats = {};
owner.forumStats.postCount = 0;
owner.forumStats.postLikes = 0;
owner.forumStats.title = "the newbie";
owner.forumStats.tag = "newbie";
owner.forumStats.img = "http://placehold.it/122x122";
changed = true;
}
if(changed){
anyUpdateToUsers = true;
Meteor.users.update({_id: owner._id}, {$set:{ forumStats:owner.forumStats }});
}
});
if(anyUpdateToUsers) // refresh it
usrs = Meteor.users.find({_id:{$in: userids}}, projectedFields);
usrs.map(function(owner){
console.log(owner);
});
return [replies, usrs];
});
It works great with the following client side:
Template.forumReplyOwner.helpers({
replyOwner: function(reply){
var owner = Meteor.users.findOne({_id: reply.owner});
console.log(reply, owner);
if(!owner || !owner.forumStats) return; // oh shait!
var r = {};
r.owner = owner._id;
r.ownerUsername = owner.username;
r.ownerPostCount = owner.forumStats.postCount;
r.ownerPostLikes = owner.forumStats.postLikes;
r.ownerTitle = owner.forumStats.title;
r.ownerTag = owner.forumStats.tag;
r.ownerImg = owner.forumStats.img;
return r;
},
ownerImgTab: function(){
return {src: this.ownerImg};
}
});
However, I am now facing another problem. Even tho I am restricting the fields I am publishing from the Users collection, it is still sending down the "services" field, that contains login data that shouldnt be sent, ideas?
Is there a way to get the results from Google Autocomplete API before it's displayed below the input? I want to show results from any country except U.S.A.
I found this question: Google Maps API V3 - Anyway to retrieve Autocomplete results instead of dropdown rendering it? but it's not useful, because the method getQueryPredictions only returns 5 elements.
This is an example with UK and US Results: http://jsfiddle.net/LVdBK/
Is it possible?
I used the jquery autocomplete widget and called the google methods manually.
For our case, we only wanted to show addresses in Michigan, US.
Since Google doesn't allow filtering out responses to that degree you have to do it manually.
Override the source function of the jquery autocomplete
Call the google autocompleteService.getQueryPredictions method
Filter out the results you want and return them as the "response" callback of the jquery autocomplete.
Optionally, if you need more detail about the selected item from Google, override the select function of the jquery autocomplete and make a call to Google's PlacesService.getDetails method.
The below assumes you have the Google api reference with the "places" library.
<script src="https://maps.googleapis.com/maps/api/js?key=[yourKeyHere]&libraries=places&v=weekly" defer></script>
var _autoCompleteService; // defined globally in script
var _placesService; // defined globally in script
//...
// setup autocomplete wrapper for google places
// starting point in our city
var defaultBounds = new google.maps.LatLngBounds(
new google.maps.LatLng('42.9655426','-85.6769166'),
new google.maps.LatLng('42.9655426','-85.6769166'));
if (_autoCompleteService == null) {
_autoCompleteService = new google.maps.places.AutocompleteService();
}
$("#CustomerAddress_Street").autocomplete({
minLength: 2,
source: function (request, response) {
if (request.term != '') {
var googleRequest = {
input: request.term,
bounds: defaultBounds,
types: ["geocode"],
componentRestrictions: { 'country': ['us'] },
fields: ['geometry', 'formatted_address']
}
_autoCompleteService.getQueryPredictions(googleRequest, function (predictions) {
var michiganOnly = new Array(); // array to hold only addresses in Michigan
for (var i = 0; i < predictions.length; i++) {
if (predictions[i].terms.length > 0) {
// find the State term. Could probably assume it's predictions[4], but not sure if it is guaranteed.
for (var j = 0; j < predictions[i].terms.length; j++) {
if (predictions[i].terms[j].value.length == 2) {
if (predictions[i].terms[j].value.toUpperCase() == 'MI') {
michiganOnly.push(predictions[i]);
}
}
}
}
}
response(michiganOnly);
});
}
},
select: function (event, ui) {
if (ui != null) {
var item = ui.item;
var request = {
placeId: ui.item.place_id
}
if (_placesService == null) {
$("body").append("<div id='GoogleAttribution'></div>"); // PlacesService() requires a field to put it's attribution image in. For now, just put on on the body
_placesService = new google.maps.places.PlacesService(document.getElementById('GoogleAttribution'));
}
_placesService.getDetails(request, function (result, status) {
if (result != null) {
const place = result;
if (!place.geometry) {
// User entered the name of a Place that was not suggested and
// pressed the Enter key, or the Place Details request failed.
//window.alert("No details available for input: '" + place.name + "'");
return;
}
else {
var latitude = place.geometry.location.lat();
var longitude = place.geometry.location.lng();
// do something with Lat/Lng
}
}
});
}
}
}).autocomplete("instance")._renderItem = function (ul, item) {
// item is the prediction object returned from our call to getQueryPredictions
// return the prediction object's "description" property or do something else
return $("<li>")
.append("<div>" + item.description + "</div>")
.appendTo(ul);
};
$("#CustomerAddress_Street").autocomplete("instance")._renderMenu = function (ul, items) {
// Google's terms require attribution, so when building the menu, append an item pointing to their image
var that = this;
$.each(items, function (index, item) {
that._renderItemData(ul, item);
});
$(ul).append("<li class='ui-menu-item'><div style='display:flex;justify-content:flex-end;'><img src='https://maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-white3.png' /></div></li>")
}
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.