Update Meteor Leaderboard with own data - javascript

I've changed the meteor example leaderboard into a voting app. I have some documents with an array and in this array there are 6 values. The sum of this 6 values works fine, but not updating and showing the values in my app.
The values are only updating, if I click on them. The problem is, that I get the booknames (it's a voting app for books) from the "selected_books" variable (previously selected_players), but I don't know how I can get the book names.
By the way: _id are the book names.
I will give you some code snippets and hope, somebody have a solution.
This is a document from my database:
{
_id: "A Dance With Dragons: Part 1",
isbn: 9780007466061,
flag: 20130901,
score20130714: [1,2,3,4,5,0],
}
parts of my html file:
<template name="voting">
...
<div class="span5">
{{#each books}}
{{> book}}
{{/each}}
</div>
...
</template>
<template name="book">
<div class="book {{selected}}">
<span class="name">{{_id}}</span>
<span class="totalscore">{{totalscore}}</span>
</div>
</template>
and parts of my Javascript file:
Template.voting.books = function () {
var total = 0;
var book = Session.get("selected_book");
Books.find({_id:book}).map(function(doc) {
for (i=0; i<6; i++) {
total += parseInt(doc.score20130714[i], 10);
}
});
Books.update({_id:book}, {$set: {totalscore: total}});
return Books.find({flag: 20130901}, {sort: {totalscore: -1, _id: 1}});
};
Thanks in advance

Don't update data in the helper where you fetch it! Use a second helper for aggregating information or a transform for modifying data items. Example:
Template.voting.books = function() {
return Books.find({}, {sort: {totalscore: -1, _id: 1}});
};
Template.books.totalscore = function() {
var total = 0;
for(var i=0; i<6; i++) {
total += this.score[i];
}
return total;
};
As a side note, DO NOT USE the construct for (i=0; i<6; i++), it's deadly. Always declare your index variables: for (var i=0; i<6; i++).

Related

How to Paginate a Computed Property in Vue

I am creating a Vue app, where a list of jobs will be displayed and this data is coming from a JSON object. In this app I also am adding filtering functionality as well as pagination. So what I have so far is:
<div id="app" v-cloak>
<h2>Location</h2>
<select v-model="selectedLocation" v-on:change="setPages">
<option value="">Any</option>
<option v-for="location in locations" v-bind:value="location" >{{ location }}</option>
</select>
<div v-for="job in jobs">
<a v-bind:href="'/job-details-page/?entity=' + job.id"><h2>{{ job.title }}</h2></a>
<div v-if="job.customText12"><strong>Location:</strong> {{ job.customText12 }}</div>
</div>
<div class="paginationBtns">
<button type="button" v-if="page != 1" v-on:click="page--">Prev</button>
<button type="button" v-for="pageNumber in pages.slice(page-1, page+5)" v-on:click="page = pageNumber"> {{pageNumber}} </button>
<button type="button" v-if="page < pages.length" v-on:click="page++">Next</button>
</div>
<script>
var json = <?php echo getBhQuery('search','JobOrder','isOpen:true','id,title,categories,dateAdded,externalCategoryID,employmentType,customText12', null, 200, '-dateAdded');?>;
json = JSON.parse(json);
var jsonData = json.data;
var app = new Vue({
el: '#app',
data() {
return {
//assigning the jobs JSON data to this variable
jobs: jsonData,
locations: ['Chicago', 'Philly', 'Baltimore'],
//Used to filter based on selected filter options
selectedLocation: '',
page: 1,
perPage: 10,
pages: [],
}
},
methods: {
setPages () {
this.pages = [];
let numberOfPages = Math.ceil(this.jobs.length / this.perPage);
for (let i = 1; i <= numberOfPages; i++) {
this.pages.push(i);
}
},
paginate (jobs) {
let page = this.page;
let perPage = this.perPage;
let from = (page * perPage) - perPage;
let to = (page * perPage);
return jobs.slice(from, to);
},
}
watch: {
jobs () {
this.setPages();
}
},
})
computed: {
filteredJobs: function(){
var filteredList = this.jobs.filter(el=> {
return el.customText12.toUpperCase().match(this.selectedLocation.toUpperCase())
});
return this.paginate(filteredList);
}
}
</script>
So the issue I am running into is that I want the amount of pages to change when the user filters the list using the select input. The list itself changes, but the amount of pages does not, and there ends up being a ton of empty pages once you get past a certain point.
I believe the reason why this is happening is the amount of pages is being set based on the length of the jobs data object. Since that never changes the amount of pages stays the same as well. What I need to happen is once the setPages method is ran it needs to empty the pages data array, then look at the filteredJobs object and find the length of that instead of the base jobs object.
The filteredJobs filtering is a computed property and I am not sure how to grab the length of the object once it has been filtered.
EDIT: Okay so I added this into the setPages method:
let numberOfPages = Math.ceil(this.filteredJobs.length / this.perPage);
instead of
let numberOfPages = Math.ceil(this.jobs.length / this.perPage);
and I found out it is actually grabbing the length of filteredJobs, but since I am running the paginate method on that computed property, it is saying there is only 10 items in the filteredJobs array currently and will only add one pagination page. So grabbing the length of filteredJobs may not be the best route for this. Possibly setting a data variable to equal the filtered jobs object may be better and grab the length of that.

Vue.js 2 - Array change detection

Here's a simplified version of my code :
<template>
/* ----------------------------------------------------------
* Displays a list of templates, #click, select the template
/* ----------------------------------------------------------
<ul>
<li
v-for="form in forms.forms"
#click="selectTemplate(form)"
:key="form.id"
:class="{selected: templateSelected == form}">
<h4>{{ form.name }}</h4>
<p>{{ form.description }}</p>
</li>
</ul>
/* --------------------------------------------------------
* Displays the "Editable fields" of the selected template
/* --------------------------------------------------------
<div class="form-group" v-for="(editableField, index) in editableFields" :key="editableField.id">
<input
type="text"
class="appfield appfield-block data-to-document"
:id="'item_'+index"
:name="editableField.tag"
v-model="editableField.value">
</div>
</template>
<script>
export default {
data: function () {
return {
editableFields: [],
}
},
methods: {
selectTemplate: function (form) {
/* ------------------
* My problem is here
*/ ------------------
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
}
}
}
</script>
Basically I want to update the array EditableFields each time the user clicks on a template. My problem is that Vuejs does not update the display because the detection is not triggered. I've read the documentation here which advise to either $set the array or use Array instance methods only such as splice and push.
The code above (with push) works but the array is never emptied and therefore, "editable fields" keep pilling up, which is not a behavior I desire.
In order to empty the array before filling it again with fresh data, I tried several things with no luck :
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
==> Does not update the display
for (let i = 0; i < form.editable_fields.length; i++) {
this.$set(this.editableFields, i, form.editable_fields[i]);
}
==> Does not update the display
this.editableFields = form.editable_fields;
==> Does not update the display
Something I haven't tried yet is setting a whole new array with the fresh data but I can't understand how I can put that in place since I want the user to be able to click (and change the template selection) more than once.
I banged my head on that problem for a few hours now, I'd appreciate any help.
Thank you in advance :) !
I've got no problem using splice + push. The reactivity should be triggered normally as described in the link you provided.
See my code sample:
new Vue({
el: '#app',
data: function() {
return {
forms: {
forms: [{
id: 'form1',
editable_fields: [{
id: 'form1_field1',
value: 'form1_field1_value'
},
{
id: 'form1_field2',
value: 'form1_field2_value'
}
]
},
{
id: 'form2',
editable_fields: [{
id: 'form2_field1',
value: 'form2_field1_value'
},
{
id: 'form2_field2',
value: 'form2_field2_value'
}
]
}
]
},
editableFields: []
}
},
methods: {
selectTemplate(form) {
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<ul>
<li v-for="form in forms.forms"
#click="selectTemplate(form)"
:key="form.id">
<h4>{{ form.id }}</h4>
</li>
</ul>
<div class="form-group"
v-for="(editableField, index) in editableFields"
:key="editableField.id">
{{ editableField.id }}:
<input type="text" v-model="editableField.value">
</div>
</div>
Problem solved... Another remote part of the code was in fact, causing the problem.
For future reference, this solution is the correct one :
this.editableFields.splice(0, this.editableFields.length);
for (let i = 0; i < form.editable_fields.length; i++) {
this.editableFields.push(form.editable_fields[i]);
}
Using only Array instance methods is the way to go with Vuejs.

How to update nested object in Ember

I'm writing an application using EmberJS 2.4. I have the following service that
generates objects of objects of random data:
export default Ember.Service.extend({
crits: {},
loadCrits() {
var newCrits = {};
var max = Math.random() * 10;
for(var i=0; i<max; i++) {
var crit = {};
var num = Math.random() * 5;
for(var j=0; j<num; j++) {
crit["data"+j] = j;
}
newCrits["crit"+i] = crit;
}
this.set("crits", newCrits);
}
});
I have a Component that displays the data in a table. For each key-value pair of each nested object, I have a button so that the user can change the value:
<table>
{{#each-in critService.crits as |key crit|}}
<tr>
<td>key</td>
<td><ul>
{{#each-in crit as |k v|}}
<li>
{{k}} = {{v}}
<button {{action "modifyCrit" crit k}}>change</button>
</li>
{{/each-in}}
</ul></td>
</tr>
{{/each-in}}
</table>
The component handles the action thusly:
export default Ember.Component.extend({
critService: Ember.inject.service(),
actions: {
modifyCrit(crit, k) {
Ember.set(crit, k, "new value");
}
}
});
The problem is that the view is not updated. If I understand correctly, this is because Ember doesn't know how to link the "crit" nested object with the "critService" from which is came. There is thus no event/observer triggered that would update the view.
How can I modify this so that when the user clicks the button, the view is updated with the new value?
You need to change data structure. I provide you a twiddle. Please check out this twiddle

Angular js comparison

I have a condition that needs to be checked in my view: If any user in the user list has the same name as another user, I want to display their age.
Something like
<div ng-repeat="user in userList track by $index">
<span class="fa fa-check" ng-if="user.isSelected"></span>{{user.firstName}} <small ng-if="true">{{'AGE' | translate}} {{user.age}}</small>
</div>
except I'm missing the correct conditional
You should probably run some code in your controller that adds a flag to the user object to indicate whether or not he/she has a name that is shared by another user.
You want to minimize the amount of logic there is inside of an ng-repeat because that logic will run for every item in the ng-repeat each $digest.
I would do something like this:
controller
var currUser, tempUser;
for (var i = 0; i < $scope.userList.length; i++) {
currUser = $scope.userList[i];
for (var j = 0; j < $scope.userList.length; j++) {
if (i === j) continue;
var tempUser = $scope.userList[j];
if (currUser.firstName === tempUser.firstName) {
currUser.showAge = true;
}
}
}
html
ng-if='user.showAge'
Edit: actually, you probably won't want to do this in the controller. If you do, it'll run every time your controller loads. You only need this to happen once. To know where this should happen, I'd have to see more code, but I'd think that it should happen when a user is added.
You can simulate a hashmap key/value, and check if your map already get the property name. Moreover, you can add a show property for each objects in your $scope.userList
Controller
(function(){
function Controller($scope) {
var map = {};
$scope.userList = [{
name:'toto',
age: 20,
show: false
}, {
name:'titi',
age: 22,
show: false
}, {
name: 'toto',
age: 22,
show: false
}];
$scope.userList.forEach(function(elm, index){
//if the key elm.name exist in my map
if (map.hasOwnProperty(elm.name)){
//Push the curent index of the userList array at the key elm.name of my map
map[elm.name].push(index);
//For all index at the key elm.name
map[elm.name].forEach(function(value){
//Access to object into userList array with the index
//And set property show to true
$scope.userList[value].show = true;
});
} else {
//create a key elm.name with an array of index as value
map[elm.name] = [index];
}
});
}
angular
.module('app', [])
.controller('ctrl', Controller);
})();
HTML
<body ng-app="app" ng-controller="ctrl">
<div ng-repeat="user in userList track by $index">
<span class="fa fa-check"></span>{{user.name}} <small ng-if="user.show">{{'AGE'}} {{user.age}}</small>
</div>
</body>

Increment A Variable In AngularJS Template

I'll preface this by saying I am very new to AngularJS so forgive me if my mindset is far off base. I am writing a very simple single page reporting app using AngularJS, the meat and potatoes is of course using the angular templating system to generate the reports themselves. I have many many reports that I am converting over from a Jinja-like syntax and I'm having a hard time replicating any kind of counter or running tabulation functionality.
Ex.
{% set count = 1 %}
{% for i in p %}
{{ count }}
{% set count = count + 1 %}
{% endfor %}
In my controller I have defined a variable like $scope.total = 0; which I am then able to access inside of the template without issue. What I can't quite figure out is how to increment this total from within an ng-repeat element. I would imagine this would look something like -
<ul>
<li ng-repeat="foo in bar">
{{ foo.baz }} - {{ total = total + foo.baz }}
</li>
</ul>
<div> {{ total }} </div>
This obviously doesn't work, nor does something like {{ total + foo.baz}}, thanks in advance for any advice.
If all you want is a counter (as per your first code example), take a look at $index which contains the current (0 based) index within the containing ngRepeat. And then just display the array length for the total.
<ul>
<li ng-repeat="item in items">
Item number: {{$index + 1}}
</li>
</ul>
<div>{{items.length}} Items</div>
If you want a total of a particular field in your repeated items, say price, you could do this with a filter, as follows.
<ul>
<li ng-repeat="item in items">
Price: {{item.price}}
</li>
</ul>
<div>Total Price: {{items | totalPrice}}</div>
And the filter function:
app.filter("totalPrice", function() {
return function(items) {
var total = 0, i = 0;
for (i = 0; i < items.length; i++) total += items[i].price;
return total;
}
});
Or, for improved reusability, a generic total filter function:
app.filter("total", function() {
return function(items, field) {
var total = 0, i = 0;
for (i = 0; i < items.length; i++) total += items[i][field];
return total;
}
});
Which would be used like:
<div>Total price: {{items | total:'price'}}</div>
I needed running total rather that plain total, so I've added upon what #TimStewart left. Here the code:
app.filter("runningTotal", function () {
return function(items, field, index) {
var total = 0, i = 0;
for (i = 0; i < index+1; i++) {
total += items[i][field];
}
return total;
};
});
To use it in column you just do:
<div>Total price: {{items | runningTotal:'price':$index}}</div>
I'm not sure I totally understand the question, but are just needing to display the total number in the object you're iterating over? Just set $scope.total to the length of your array (bar in your example above). So, $scope.total = $scope.bar.length;
If you're wanting the total of all the foo.baz properties, you just need to calculate that in your controller.
$scope.total = 0;
angular.forEach($scope.bar, function(foo) {
$scope.total += foo.baz;
});

Categories