Im triying implement a reactive search function for my first App in Meteor using Meteor:Search-source package. The search function is working fine. I have a input text search field, and 2 list. The first part called "search result" get the items found when we search for some word (for example try with london), and the second part get all items in my Collection. These items are linked to a reactive helper/template function. The reactivity when I get all items is working fine if I voteUp or Down. I can see in realtime how it is updated. But when I want upvote or downvote some item found in my search, upvote and downvote is not updating, is like the items found, lost the reactivity (I can see that if I upvote or downvote in some item in the first section ("search results") the items in second list (all items) is updating right....
this is my App My App
You can check it out the issue key in "london" in search field and upvote or downvote to see that in second list the item is correctly updated, but is not working if you upvote or downvote in "search result" section (no reactive).
CLIENT
// helper function
Template.searchResult.helpers({
getItems: function() {
return itemSearch.getData({
transform: function(matchText, regExp) {
return matchText.replace(regExp, "$&")
},
sort: {upvote: -1}
});
},
isLoading: function() {
return itemSearch.getStatus().loading;
}
});
//This line return all documents by default (when empty searchbox text is empty)
Template.searchResult.rendered = function() {
itemSearch.search('');
};
SERVER
SearchSource.defineSource('items', function(searchText, options) {
var options = {sort: {upvote: -1}, limit: 20};
// var options = options || {};
if(searchText) {
var regExp = buildRegExp(searchText);
/*var selector = {title: regExp, description: regExp};*/
var selector = {$or: [
{title: regExp},
{description: regExp}
]};
return Websites.find(selector, options).fetch();
} else {
return Websites.find({}, options).fetch();
}
});
function buildRegExp(searchText) {
var words = searchText.trim().split(/[ \-\:]+/);
var exps = _.map(words, function(word) {
return "(?=.*" + word + ")";
});
var fullExp = exps.join('') + ".+";
return new RegExp(fullExp, "i");
}
HTML
<!-- template that displays searched website items -->
<template name="searchResult">
<div class="container">
<div class="jumbotron">
<h3> Search results </h3>
<ol>
{{#each getItems}}
{{> website_item}}
{{/each}}
</ol>
</div>
</div>
<!-- template that displays individual website entries -->
<template name="website_item">
<li>
{{title}}
<p>
{{description}}
</p>
<a href="#" class="btn btn-default js-upvote">
<span class="glyphicon glyphicon-arrow-up" aria-hidden="true"> </span>
</a>
<a href="#" class="btn btn-success">
{{upvote}}
</a>
<a href="#" class="btn btn-default js-downvote">
<span class="glyphicon glyphicon-arrow-down" aria-hidden="true"></span>
</a>
<a href="#" class="btn btn-danger">
{{downvote}}
</a>
<p>
Created On: {{createdOn}}
</p>
<a href="/details/{{_id}}" class="btn btn-success js-description">
view web description
</a>
<!-- you will be putting your up and down vote buttons in here! -->
</li>
Some suggestion?? Many thanks in advance!
The returned data records themselves are not reactive, as the data is fetched internally using a method call (search.source).
You get a snapshot of the data as it was when you searched.
In addition, the data is cached by default, so subsequent searches for the same term don't trigger requests to the server for a certain period of time. You can adjust the time period via the keepHistory option.
Therefore, you will not get reactive changes via the package and it does not seem like an adequate solution for your situation.
You could try and get the data via a subscription and map the data returned from the search to the records in your collection but that seems to be costly.
Also, see this issue, which demonstrates that others had encountered the same type of issue.
Related
When I add a new button with some value it gets dynamically added into DOM. Non-Angular HTML element for this button is:
<li class="ui-state-default droppable ui-sortable-handle" id="element_98" data-value="2519">
25.19 EUR
<button type="button" class="btn btn-default removeParent">
<span class="glyphicon glyphicon-remove" aria-hidden="true">
</span>
</button>
</li>
Once I remove this button I want to check it is not present anymore. Element that I'm searching for is data-value="2519"and this could be anything I set, like for example 2000, 1000, 500, 1050,...
In page object file I have tried to use the following:
this.newValueButtonIsNotPresent = function(item) {
newValueButton = browser.element(by.id("containerQUICK_ADD_POINTS")).all(by.css('[data-value="' + item + '"]'));
return newValueButton.not.isPresent();
};
And in spec file I call this function as follows:
var twentyEurosButtonAttributeValue = '2000';
describe("....
it ("...
expect(predefined.newValueButtonIsNotPresent(twentyEurosButtonAttributeValue)).toBeTruthy();
I know this is not correct, but how I can achieve something like that or is there another way?
Stupid me, I found a simple solution. Instead dynamically locating an element I located the first on the list, which is always the one, which was newly added and then checked if it's text does not match:
Page object file:
this.newValueButtonIsNotPresent = function() {
newValueButton = browser.element(by.id("containerQUICK_ADD_POINTS")).all(by.tagName('li')).first();
return newValueButton.getText();
};
Spec file:
// verify element 20.00 EUR is not present
predefined.newValueButtonIsNotPresent().then(function(value) {
expect(value).not.toEqual(twentyEurosText);
});
I am brand new to coding so forgive my very obvious ignorance. My question is this: How can I create a unique variable for each item in a global array in MongoDB so that I can tally upvotes and downvotes and sort accordingly. I'm doing all this in the Meteor framework.
Here's my code:
<template name="website_item">
<li>
{{title}}
<p>
{{description}}
</p>
<a href="#" class="btn btn-default js-upvote">
<span class="glyphicon glyphicon-arrow-up" aria-hidden="true"> </span>
</a>
<a href="#" class="btn btn-default js-downvote">
<span class="glyphicon glyphicon-arrow-down" aria-hidden="true"> </span>
</a>
<p>
Votes: {{totalVotes}}
</p>
</li>
</template>
Here's my client.js:
var totalVotes = 0;
Template.website_item.events({
"click .js-upvote":function(event){
// example of how you can access the id for the website in the database
// (this is the data context for the template)
var website_id = this._id;
console.log("Up voting website with id "+website_id);
// put the code in here to add a vote to a website!
totalVotes++;
console.log(totalVotes);
Websites.update({_id:website_id}, {$set: {totalVotes:totalVotes}});
return false;// prevent the button from reloading the page
},
"click .js-downvote":function(event){
// example of how you can access the id for the website in the database
// (this is the data context for the template)
var website_id = this._id;
console.log("Down voting website with id "+website_id);
// put the code in here to remove a vote from a website!
totalVotes--;
console.log(totalVotes);
Websites.update({_id:website_id}, {$set: {totalVotes:totalVotes}});
return false;// prevent the button from reloading the page
}
})
In collection.js I have:
Websites = new Mongo.Collection("websites");
and in server.js I have:
import { Meteor } from 'meteor/meteor';
Meteor.startup(() => {
// code to run on server at startup
if (!Websites.findOne()){
console.log("No websites yet. Creating starter data.");
Websites.insert({
title:"Test site",
url:"http://www.test.com",
description:"This is a test.",
createdOn:new Date(),
totalVotes: 0
});
Websites.insert({
title:"Google",
url:"http://www.google.com",
description:"Popular search engine.",
createdOn:new Date(),
totalVotes: 0
});
}
});
I hope i've been comprehensive and clear with my question. I just want to be able to save up and downvote tallies for each item in my global array but right now there's just a single variable which does nothing for me.
Thanks so much!
you're missing publish and subscribe. the idea is your template will subscribe to the website data, and the server will then publish what you're asking for. you would write a helper that performs a find() on the published data, and an #each loop that would iterate over that data.
once you're done that, the tricky part, which is what you're asking about, is to tag each loop item so that, when clicked, you can uniquely identify it in the event handlers.
let's set up a new template (i'm writing all this code just in this text box, w/o trying it, so please forgive typos):
<template name="Websites">
{{#each website in websites}}
{{website.title}}
Votes: {{website.totalVotes}}
<button class="js-upvote" data-id={{website._id}}>Vote Up</button>
{{/each}}
</template>
then, you need to subscribe, like this:
Template.Websites.onCreated(function() {
this.subscribe('websites');
});
i'll assume you already have a publish written, or auto publish is on...
then write the helper:
Template.Websites.helpers({
websites() {
return Websites.find({});
}
});
and finally the event listener that can identify which item was clicked:
Template.Websites.events({
'click .js-upvote': function(event, template) {
event.preventDefault();
if (event && event.currentTarget && event.currentTarget.dataset) {
let websiteId = event.currentTarget.dataset.id;
// now you can save the incremented votes for this website
}
}
});
Regarding your totalVotes variable, i think i understand what you're trying to do, and now you can get rid of that. with the code i've written, it will save to the db the increments and decrements of each website, and because you're subscribed, you'll get that updated vote total back and reactively display it in the template.
update:
alternatively, accessing _id without writing it to the DOM:
<template name="Websites">
{{#each website in websites}}
{{website.title}}
Votes: {{website.totalVotes}}
<button class="js-upvote">Vote Up</button>
{{/each}}
</template>
Template.Websites.events({
'click .js-upvote': function(event, template) {
event.preventDefault();
let websiteId = this._id;
// now you can save the incremented votes for this website
}
}
});
Note: Whole code can be found here:
https://github.com/Julian-Th/crowducate-platform/tree/feature/courseEditRights
Currently, all items from an array are displayed in one single list instead of a separate list tag:
My JS (I commented out some prior approaches):
Template.modalAddCollaborators.events({
'click #js-addCollaborator' : function (event) {
var collaboratorName = $('#collaboratorName').val(); //
Courses.update(
{ _id: this._id },
{ $addToSet: {canEditCourse: collaboratorName } }
)
$('#collaboratorName').val("");
}
});
Template.modalAddCollaborators.helpers({
'addedCollaborators': function () {
return Courses.find();
//return Courses.find({_id: this._id}, {$in: "canEditCourse"});
//return Courses.distinct("canEditCourse");
}
});
My HTML:
<template name="modalAddCollaborators">
<div id="modalAddCollaborators" class="modal fade" role="dialog">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">×</button>
<h4 class="modal-title">Manage Your Collaborators</h4>
</div>
<div class="modal-body">
<form class="form" role="form">
<ul class="list-group">
{{#each addedCollaborators}}<li class="list-group-item">{{canEditCourse}}</li>{{/each}}
</ul>
<div class="form-group">
<input type="text" id="collaboratorName" placeholder="add a collaborator ...">
<button type="button" id="js-addCollaborator" class="btn btn-success">Add</button>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</template>
MY JSON:
{
"_id" : "rmZEFmfoBwf4NwqX4",
"title" : "Love",
"coverImageId" : "P7PyR6x64uCSX7X9m",
"author" : "test",
"keywords" : [
"test"
],
"published" : "true",
"about" : "test",
"canEditCourse" : [
"wicazRk3EsThE5E8W",
"Jolle",
"jolle",
"vW59A6szZijMDLDNh"
],
"createdById" : "wicazRk3EsThE5E8W",
"dateCreated" : ISODate("2015-12-27T15:06:28.272Z")
}
Any help appreciated, thank you.
Courses.find(); returns a cursor and not an array. Use fetch() method instead:
Template.modalAddCollaborators.helpers({
'addedCollaborators': function () {
return Courses.find().fetch();
}
});
In your template, create nested {{#each}} blocks with the first one iterating over the courses array and the next each block getting the canEditCourse array as the parameter. Inside the block, you can use this to reference the element being iterated over, something like the following for example:
<template name="modalAddCollaborators">
{{#each addedCollaborators}}
<h1>{{title}}</h1>
<ul class="list-group">
{{#each canEditCourse}}
<li class="list-group-item">{{this}}</li>
{{/each}}
</ul>
{{/each}}
</template>
It looks like you are storing two types of values in the canEditCourse:
String - Meteor.userId
String - username
It may be good to store either the userId or the username, but perhaps not both.
UserID solution
In this approach, you store the User IDs in the canEditCourse array, and then use a collection helper to retrieve the username for display:
Courses.helpers({
"getCollaboratorUsernames": function () {
// Get collaborator IDs array
var userIds = this.canEditCourse;
// Get the users, using MongoDB '$in' operator
// https://docs.mongodb.org/v3.0/reference/operator/query/in/
var users = Meteor.users.find({_id: {$in: userIds}).fetch();
// placeholder array for usernames
var collaboratorUsernames = []
// Get username for each user, add it to usernames array
users.forEach(function (user) {
// Add current username to usernames array
collaboratorUsernames.push(user.profile.username);
});
return collaboratorUsernames;
}
});
Also, it may be cleaner if the template helper were only to return the array of userIds, as opposed to a course object (Courses.find().fetch()).
Inputting UserIDs
You may choose a typeahead approach for inputting user IDs, similar to how courses are categorized in Crowducate.
Note: you will need a publication and subscription to make usernames/IDs available for the Selectize input.
Displaying Usernames
The other key component will be how to display the usernames as separate Boodstrap tag elements. You can iterate over the returned collaboratorUsernames array like so:
{{# each getCollaboratorUsernames }}
<span class="label label-info">{{ this }}</span>
{{/ each }}
Note: make sure the course collaborator users are available via a publication/subscription:
In server code:
Meteor.publish('courseCollaborators', function (courseId) {
// Get the course object
var course = Courses.findOne(courseId);
// Get course collaborator IDs
var collaboratorIds = course.canEditCourse;
// Consider renaming the 'canEditCourse' field to 'collaboratorIds'
// Then, it would look like
// var courseCollaboratorIds = course.collaboratorIds;
// Or, you could even skip that, and the code would still be literate
// Get course collaborators
var collaborators = Meteor.users.find({_id: {$in: collaboratorIds}).fetch();
return collaborators;
});
Then, in your template.created callback:
Template.modalAddCollaborators.created = function () {
// Get reference to template instance
var instance = this;
// Get reference to router
var route = Router.current();
// Get course ID from route
var courseId = route.params._id;
// Subscribe to Course Collaborators, template level
instance.subscribe("courseCollaborators", courseId);
};
Be sure to wrap all of your code for creating the Selectize widget in an if (instance.subscriptionsReady()) {} block:
Template.modalAddCollaborators.rendered = function () {
// Get reference to template instance
var instance = this;
// Make sure subscriptions are ready before rendering Selectize
if (instance.subscriptionsReady()) {
// Get course collaborator usernames/IDs
// Render the Selectize widget
// User should see usernames
// UserID is saved to collection
}
};
I'm a super beginner who just starting ionic Framework & Firebase. I know that there are many problems in the code that I wrote.
However, please excuse me cuz I'm still in learning.
The function that I planned is this - If you've ever pressed the button, I wanna show you the 'black heart' when you signed it. If not, you'll see the 'white heart'.
firebase structure
comments{
message: "hi~",
user: "user name",
userId: "user Id",
userImgURI: "https://fbcdn-profile-a.akamaihd.net/hprofile-a..",
like{
-JmvN2CHvAOcBQCp2r0{
uid: "facebook:69843548512"
}
}
}
html
<div class="item item-avatar item-icon-right" ng-repeat="comment in comments">
<img ng-src="{{comment.userImgURI}}">
<span class="commentUserName"><b>{{comment.user}}</b></span>
<span class="commentMessage">{{comment.message}}</span>
<i class="icon ion-chevron-right ion-trash-a" ng-show="comment.userId == authData.uid" ng-click="removeComment(comment.$id)"></i>
<div ng-if="comment.like == null">
<button ng-click="likeFunction({{comment}})" class="button button-light ion-ios-heart-outline likeBt"></button>
</div>
<div ng-repeat="like in comment.like">
<div ng-if="like.uid == authData.uid">
<button onclick="alert('You have already like.')" class="button button-light ion-heart"></button>
</div>
<div ng-show="like.uid != authData.uid">
<button ng-click="likeFunction(comment)" class="button button-light ion-ios-heart-outline likeBt"></button>
</div>
</div>
</div>
controller
var commentsRef = new Firebase('https://choifirstproject.firebaseio.com/products/' + $scope.selectProductKey + '/comments');
$scope.comments = $firebaseArray(commentsRef);
$scope.likeFunction = function (comment) {
var ref = new Firebase('https://choifirstproject.firebaseio.com/products/' + $scope.selectProductKey + '/comments/' + comment.$id);
if ($rootsScope.authData) {
ref.child('like').push({
uid: $rootScope.authData.uid
});
}else{
alert('Please login..');
}
}
The problem
The problem is this.
There's no problem if one user pressed the heart button. But when more than two users pressed it, the following problem happens.
The output of heart button is duplicated as many as the number of peaple who pressed the button.
I just want this;
(1) If you've ever pressed the button, you'll see just 'one' black heart.
(2) If not, you'll see one 'white heart'.
What should I do? I'd appreciate some help.
Get rid of your ng-repeat instead attach a function to the $scope that returns true if the current user has pressed on the heart. (Use the same logic as you do in your ng repeat but in plain java script) Then use ng-show ng-hide using the function that you wrote to decide which heart to show.
Alternatively you could do something like this :
<li class="animate-repeat" ng-repeat="friend in friends | filter:q as results">
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
</li>
<li class="animate-repeat" ng-if="results.length == 0">
<strong>No results found...</strong>
Have a filter so that you only get results of like.uid == authData.uid
and have an if results.length == 0 show the empty hart if results.length>0 show the full heart
Rather than using push(), which creates a random, unique, chronological id, just store them by the users' uid.
ref.child('like').child($rootScope.authData.uid).set(true);
Now you can refer to the values by user to see if their flag should be on or off. You'll probably also want to keep a total count of likes and that would be done with a transaction:
ref.child('total_likes').transaction(function(currentValue) {
return (currentValue||0)+1;
});
I'm just playing around with different patterns and am very new to programming, however I've got everything to work in my test app so far except this. I've tried a bunch of variations with no luck, but I suspect I'm missing something really simple.
Basically what I want to happen is for a user to click a button and for it to then update the value of two specific attributes of the current object.
In this example I'm wanting the update to occur when the user clicks the "Return" button (the other buttons shown below are working fine).
Here's the HTML template for the button in question:
<template name="bookDetails">
<div class="post">
<div class="post-content">
<h3>{{title}}</h3><span> {{author}}</span>
{{#if onLoan}}
<i class="fa fa-star"></i>
On loan to: {{lender}}{{/if}}
</div>
{{#if ownBook}}
Edit
Lend
<div class="control-group">
<div class="controls">
<a class="discuss btn return" href="">Return </a>
</div>
</div>
{{/if}}
</div>
</template>
Here's the .js file which contains my Template event. Basically I want to set the values for the "lendstatus" and "lender" attributes.
Template.bookDetails.helpers({
ownBook: function() {
return this.userId == Meteor.userId();
},
onLoan: function() {
return this.lendstatus == 'true';
}
});
Template.bookLoan.events({
'click .return': function(e) {
e.preventDefault();
var currentBookId = this._id;
var bookProperties = {
lendstatus: "false",
lender: "",
}
Books.update(currentBookId, {$set: bookProperties}, function(error) {
if (error) {
// display the error to the user
throwError(error.reason);
} else {
Router.go('bookPage', {_id: currentBookId});
}
});
},
});
If I type the following into the Browser console while on the page for the object with id ZLDvXZ9esfp8yEmJu I get the correct behaviour on screen and the database updates so I know I'm close:
Books.update({ _id: "ZLDvXZ9esfp8yEmJu"}, {$set: {lendstatus: "false", lender: ""}});
What am I missing?
OK - so my problem was that I'd defined the event handler in the wrong template. I'd defined it in the bookLoan template instead of the bookDetails template. Thanks #saimeunt for pointing this out!