Help! Something very strange is happening within my app.
I have a merchantReview template that is supposed to show the number of reviewers.
Below is what the template looks like:
<template name="merchantReview">
<div> {{numberOfReviewers}} Total</div>
And what the helper code looks like:
Template.merchantReview.helpers({
'numberOfReviewers': function () {
var numberOfReviewers = 0;
var merchantProfileId = Session.get('offerUserId2');
console.log("The numberOfReviews _id session is: " + merchantProfileId );
merchantReviews.find({_id: merchantProfileId}).map(function (doc)
{
numberOfReviewers += doc.noOfReviewers;
});
return numberOfReviewers;
}
});
This yields nothing on the merchantReview page at all.
Lets have a look at the Router:
Router.route('/merchantReview/:_id', {
template: 'merchantReview',
data: function(){
},
waitOn: function(){
return Meteor.subscribe('merchantReviews');
}
});
And what am subscribing to:
Meteor.publish('merchantReviews', function publishmerchantReviews(){
return merchantReviews.find();
});
Now for the "Something very strange" part.
When I run this following code (which is similar to the helper code) in the chrome browser console, I get mixed results.
When I run the below:
var numberOfReviewers = 0;
var merchantProfileId = Session.get('offerUserId2');
console.log("The _id is: " + merchantProfileId );
merchantReviews.find({_id: merchantProfileId}).map(function (doc)
{
numberOfReviewers += doc.noOfReviewers;
});
console.log (numberOfReviewers);
...it yields:
The _id is: udEnfEmy5DSBvDsSy
0
As you see, it bypasses the merchantReviews.find query.
However when slightly alter the code, and run:
var numberOfReviewers = 0;
var merchantProfileId = Session.get('offerUserId2');
console.log("The _id is: " + merchantProfileId );
merchantReviews.find({_id: "udEnfEmy5DSBvDsSy"}).map(function (doc)
{
numberOfReviewers += doc.noOfReviewers;
});
console.log (numberOfReviewers);
This time it yields:
The _id is: udEnfEmy5DSBvDsSy
93
Strange isn't it?
Can anyone explain why the
merchantReviews.find({_id: merchantProfileId})... query doesn't recognize the merchantProfileId variable which holds the udEnfEmy5DSBvDsSy value? And how do I fix this?
The reason why the variable wasn't being recognized in the query is because the variable was in array form instead of string form.
I realized this by console.log("The _id is: " + merchantProfileId );
This would sometimes yield:
["udEnfEmy5DSBvDsSy"]
I resolved his by sanitizing the variable to ensure its a string not an array using the toString() function. So in code its : merchantProfileId.toString();
Following is the full resolution in code:
var numberOfReviewers = 0;
var merchantProfileId = Session.get('offerUserId2');
// Below immediately sanitize the variable ensuring it is a string
// before its used in the query.
merchantProfileId = merchantProfileId.toString();
console.log("The _id is: " + merchantProfileId );
merchantReviews.find({_id: merchantProfileId}).map(function (doc)
{
numberOfReviewers += doc.noOfReviewers;
});
console.log (numberOfReviewers);
This yields:
The _id is: udEnfEmy5DSBvDsSy
104
You can simplify and speed up your helper by just using .count() on the cursor. If, as you mentioned in the comments, your offerUserId2 session variable returns an array then you can just reference its first element with [0]:
Template.merchantReview.helpers({
numberOfReviewers() {
return merchantReviews.find(Session.get('offerUserId2')[0]).count();
}
})
Your problem is a classic javascript async gotcha. Looking at your code,
merchantReviews.find({_id: "udEnfEmy5DSBvDsSy"}).map(function (doc)
{
numberOfReviewers += doc.noOfReviewers;
});
console.log (numberOfReviewers);
The adding to numberOfReviewers happens in the callback for merchantReviews.find() - which usually happens at some later time. Your console.log likely happens before that, which explains why you get wierd results. If you change your code to this, you will get consistent results.
merchantReviews.find({_id: "udEnfEmy5DSBvDsSy"}).map(function (doc)
{
numberOfReviewers += doc.noOfReviewers;
console.log (numberOfReviewers);
});
Think of it this way, in javascript time moves to the right, not down the page. So indented code that is part of a callback (and indented to the right) happens after the code at the bottom.
Related
UPDATE
This issue is already discussed in github here
I am using tagsinput with typeahead in bootstrap 3. The problem which I am experiencing is with the value in case if user selects the existing tag. Display text shows it right but .val() returns its actual object. Below is the code
$('#tags').tagsinput({
//itemValue: 'value',
typeahead: {
source: function (query) {
//tags = [];
//map = {};
return $.getJSON('VirtualRoomService.asmx/GetTags?pid=' + $("#<%=hdnPID.ClientID%>").val() + '&tok=' + query)
//, function (data) {
// $.each(data, function (i, tag) {
// map[tag.TagValue] = tag;
// tags.push(tag.TagValue);
// });
// return process(tags);
//});
}
}
//freeElementSelector: "#freeTexts"
});
The problem with above code is that it results as below while fetching tags from web method
This happens when user select the existing tag. New tags no issues. I tried setting itemValue & itemText of tagsinput but not worked. Hence I decided a work-around of this problem. Since I could able get the json string as ['IRDAI", Object], if can somehow parse these object & get the actual tag value then I get the expected result of the code I am looking at.
Below is what it appears in tags input as [object Object] for text selected by user from auto populated drop down
[![enter imt
If I i specify TagId & TagValue to itemValue & itemText as below code
$('#tags').tagsinput({
itemValue: 'TagId',
itemText: 'TagValue',
typeahead: {
source: function (query) {
//tags = [];
//map = {};
return $.getJSON('VirtualRoomService.asmx/GetTags?pid=' + $("#<%=hdnPID.ClientID%>").val() + '&tok=' + query)
//, function (data) {
// $.each(data, function (i, tag) {
// //map[tag.TagValue] = tag;
// tags.push(tag.TagValue);
// });
//});
// return process(tags);
}
}
//freeElementSelector: "#freeTexts"
});
Then the result is displaying as below when below code is executed
var arr = junit.Tags.split(',');
for (var i = 0; i < arr.length; i++) {
$('#tags').tagsinput('add', arr[i]);
}
Given your example JSON response from your data source:
[
{"TagId":"1", "TagValue":"eSign"},
{"TagId":"2", "TagValue":"eInsurance Account"}
]
You'll need to tell tagsinput how to map the attributes from your response objects using itemValue and itemText in your tagsinput config object. It looks like you may have started down that path, but didn't reach the conclusion, which should look something like:
$('#tags').tagsinput({
itemValue: 'TagId',
itemText: 'TagValue',
typeahead: {
source: function (query) {
return $.getJSON('VirtualRoomService.asmx/GetTags?pid=' + $("#<%=hdnPID.ClientID%>").val() + '&tok=' + query);
}
}
});
Be sure to checkout the tagsinput examples.
This may not be the clean solution but I got around this issue through below parsing method. Hope this helps someone.
var items = $('#tags').tagsinput("items");
var tags = '';
for(i = 0; i < items.length; i++)
{
if(JSON.stringify(items[i]).indexOf('{') >= 0) {
tags += items[i].TagValue;
tags += ',';
} else {
tags += items[i];
tags += ',';
}
}
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.
I have a handlebars helper below that shows me the selected players threat. And it works.
Handlebars.registerHelper('sthreat', function() {
var sthreat = Meteor.users.findOne({_id: Session.get('selectedPlayer')}, {threat: 1});
return sthreat;
});
However the button below (on client) and method (on server) are suppose to check that the player has 5 or more threat before proceeding, though they're not working.
'click input.increment': function(){
var selectedThreat = Meteor.users.findOne({_id: Session.get('selectedPlayer')}, {threat: 1});
Meteor.call('incclick',selectedThreat);
},
incclick: function (selectedThreat) {
if(selectedThreat <= 4) {
} else {
Meteor.users.update({_id: Session.get('selectedPlayer')}, {$inc: {'threat': -5}});
Meteor.users.update({_id: this.userId}, {$inc: {'threat': 5}});
}
},
I figured if I could get the value to show in a helper, I should be able to use it in an equation. Is there something I am missing here?
Meteor.users.findOne will return a document. You passed in {threat: 1} as options, but they will be ignored since none of them are valid. Options like sort, field and limit would work.
I think you're looking to get an attribute of the document. The result of Meteor.users.findOne will be something like this
{
_id : ..
threat: 3,
...
}
So you can just pass the attribute to the method instead of the entire document:
Meteor.call('incclick',selectedThreat.threat);
I'm writing a small application that shows live hits on a website. I'm displaying the hits as a table and passing each one to a template helper to determine the row's class class. The idea is that over time hits will change colour to indicate their age.
Everything renders correctly but I need to refresh the page in order to see the helper's returned class change over time. How can I make the helper work reactively?
I suspect that because the collection object's data isn't changing that this is why and I think I need to use a Session object.
Router:
Router.route('/tracked-data', {
name: 'tracked.data'
});
Controller:
TrackedDataController = RouteController.extend({
data: function () {
return {
hits: Hits.find({}, {sort: {createdAt: -1}})
};
}
});
Template:
{{#each hits}}
<tr class="{{ getClass this }}">{{> hit}}</tr>
{{/each}}
Helper:
Template.trackedData.helpers({
getClass: function(hit) {
var oneMinuteAgo = Date.now() - 1*60*1000;
if (hit.createdAt.getTime() > oneMinuteAgo) {
return 'success';
} else {
return 'error';
}
}
});
I've managed to get this working though I'm not sure it's the 'right' way to do it. I created a function that is called every second to update Session key containing the current time. Then, in my helper, I can create a new Session key for each of the objects that I want to add a class to. This session key is based upon the value in Session.get('currentTime') and thus updates every second. Session is reactive and so the template updates once the time comparison condition changes value.
var updateTime = function () {
var time = Date.now();
Session.set('currentTime', time);
setTimeout(updateTime, 1 * 1000); // 1 second
};
updateTime();
Template.trackedData.helpers({
getClass: function(hit) {
var tenMinutesAgo = Session.get('currentTime') - 10*1000,
sessionName = "class_" + hit._id,
className;
className = (hit.createdAt.getTime() > tenMinutesAgo) ? 'success' : 'error';
Session.set(sessionName, className);
return Session.get(sessionName);
}
});
Update
Thanks for the comments. The solution I ended up with was this:
client/utils.js
// Note that `Session` is only available on the client so this is a client only utility.
Utils = (function(exports) {
return {
updateTime: function () {
// Date.getTime() returns milliseconds
Session.set('currentTime', Date.now());
setTimeout(Utils.updateTime, 1 * 1000); // 1 second
},
secondsAgo: function(seconds) {
var milliseconds = seconds * 1000; // ms => s
return Session.get('currentTime') - milliseconds;
},
};
})(this);
Utils.updateTime();
client/templates/hits/hit_list.js
Template.trackedData.helpers({
getClass: function() {
if (this.createdAt.getTime() > Utils.secondsAgo(2)) {
return 'success';
} else if (this.createdAt.getTime() > Utils.secondsAgo(4)) {
return 'warning';
} else {
return 'error';
}
}
});
In my meteor app, I'm doing infinite scrolling so I must have a limit set on my subscriptions in order to achieve this, but I also need to show the total count from that same collection and update it if the count changes. How would I achieve that?
I have a partial solution that achieves this but only returns the paginated counts, as follows:
function getTotalCount(system) {
var user = Meteor.user(),
queryObject = getSystemQueryObject(system);
if (queryObject) {
var query = queryObject.find({$and: [
{user: user.username},
{status: {$nin: status_complete}}
]});
var count = 0;
var handle = query.observeChanges({
added: function (id, user) {
$('#' + system + 'Count').text(++count);
$('#' + system + 'Count').addClass("ma-count-badge");
console.log(system + " count incremented: " + count);
},
removed: function () {
$('#' + system + 'Count').text(--count);
$('#' + system + 'Count').addClass("ma-count-badge");
console.log(system + " count decremented: " + count);
}
});
}
else {
return 0;
}
}
The other way is to push this method up to the server as a server method, but then it is not reactive, as follows:
Meteor.call('getSystemsTotalCount', system, function (err, counted) {
if (err) {
throw err;
}
if (counted > 0) {
$('#' + system + 'Count').text(counted);
$('#' + system + 'Count').addClass("ma-count-badge");
}
Session.get('listLimit');
});
}
you can to get the count with following way
var count= queryObject.find({$and: [
{user: user.username},
{status: {$nin: status_complete}}
]}).count();
after that, you can to save in a Session
Session('countCollection',count);
if the collection changes your Session also it will do
So the trick really is to combine both options I have in the question. The first part adds reactivity to adds/deletes and the second part calls out to the server to go get the total count. Now my code looks like:
query.observeChanges({
added: function(id, user) { updateListCount(system);},
removed: function(id) { updateListCount(system);}
});
And I just wrap the Method.call from the second excerpt above with a function called updateListCount invoked by the above. This addresses the issue.