This part right here is where I'm struggling.
Template.Calculator.viewmodel({
a: 0,
b: 0,
c: 0,
total: function(){
return parseInt(this.a(), 10) + parseInt(this.b(), 10) + parseInt(this.c(), 10);
}
})
instead of having the variables equal 0, how can I have them pull from the collection the value that was saved when either first submitted or after an update occurred? So for example, let's say I saved a as 7 last time and b as 6, it would load the page with a: 6 b:7 c:0 and calculate. I am having the hardest time trying to pull that data from the collection.
Thanks for any help you can provide!
There are many ways of doing it.
If you know the data subscription is going to be ready by the time the template is created you can just do:
Template.Calculator.viewmodel(
function() {
var dv = DefaultValues.findOne();
return {
a: dv.a,
b: dv.b,
c: dv.c,
total: function () {
return this.a() + this.b() + this.c();
}
}
}
);
If you have many default values you can use the document when you create the view model:
Template.Calculator.viewmodel(
function() {
// Add all properties to the view model
return DefaultValues.findOne(); // or pluck the values
},
{
total: function () {
// We're assuming the default document will have properties a, b, and c.
return this.a() + this.b() + this.c();
}
}
);
If you don't know when the data is ready then you have the problem of maybe overwriting the user's values. It all depends on where you're subscribing to the data and when the subscription is ready. The following example subscribes when the template is created and updates the values as soon as the data is ready.
Template.Calculator.viewmodel({
a: 0,
b: 0,
c: 0,
total: function () {
return this.a() + this.b() + this.c();
},
onCreated: function (template) {
var that = this;
template.subscribe('defaultData', function(){
var dv = DefaultValues.findOne();
that.a(dv.a);
that.b(dv.b);
that.c(dv.c);
})
}
});
Again, it all depends on how you're getting the data so you might end up doing something different than these.
See Defining View Models for more information.
Related
I have two collections A and B in Meteor. For A I have a publication where I filter out a range of documents in A. Now I want to create a publications for B where I publish all documents in B that have a field B.length matching A.length.
I have not been able to find any example where this is shown but I feel it must be a standard use case. How can this be done in Meteor?
This is a common pattern for reywood:publish-composite
import { publishComposite } from 'meteor/reywood:publish-composite';
publishComposite('parentChild', {
const query = ... // your filter
find() {
return A.find(query, { sort: { score: -1 }, limit: 10 });
},
children: [
{
find(a) {
return B.find({length: a.length });
}
}
]
});
This is a quite different pattern than serverTransform as on the client you end up with two collections, A and B, as opposed to a synthetic single collection A that has some fields of of B. The latter is more like a SQL JOIN.
Use serverTransform
Meteor.publishTransformed('pub', function() {
const filter = {};
return A.find(filter)
.serverTransform({
'B': function(doc) {
return B.find({
length: doc.length
}); //this will feed directly into miniMongo as if it was a seperate publication
}
})
});
So, meteor reruns helper code every time it is called in the template, right? My issue is that I have a heavy helper that returns a large object. I'm iterating over a list of these objects and then over some nested objects which is resulting in a really big lag.
So, are there any design patterns that prevent recalling the whole helper every time? Or do I just need to break up my object?
Template.deliveries.helpers({
current_delivery: function() {
var delivery_id = Template.instance().data.current_delivery_id;
var delivery = Deliveries.findOne({'_id': delivery_id});
var project = Projects.findOne({'_id':Session.get('current_project_id')});
var secondary_profile_names = [];
if (Session.get('delivery_include_secondaries')) {
for (var n in project.delivery_profiles) {
if (project.delivery_profiles[n].name === delivery.delivery_profile) {
if (project.delivery_profiles[n].secondary_deliverables) {
secondary_profile_names = project.delivery_profiles[n].secondary_deliverables;
}
break;
}
}
}
$("#delivery-profile").val(delivery.delivery_profile);
var elements = $.map(delivery.elements, function(id, idx) {
i_el = InternalElements.findOne({'_id': id});
i_el.source_element = SourceElements.findOne({'_id':i_el.source_element});
if (secondary_profile_names) {
i_el.secondary_elements = InternalElements.find({
'source_element':i_el.source_element._id,
'name':{'$in': secondary_profile_names},
"$or": [{'is_primary':false}, {'is_primary': {'$exists':false}}]
},{
'sort':{'version':-1},
'limit':1
}).fetch();
} else {
i_el.secondary_elements = [];
}
return i_el;
});
delivery.elements = elements.sort(function(a,b) { return (a.shot_name > b.shot_name) - (a.shot_name < b.shot_name); });
return delivery;
},
});
A pattern I've used successfully is to cache the results of expensive computations in a local collection.
MyLocalCache = new Mongo.Collection();
I like to make objects in this collection 1:1 with the original object so I reuse the _id from the original along with any keys and values that don't require transformation then extend the object with the transformed values.
Here is a modified leaderboard:
http://meteorpad.com/pad/CgEG3uYBZDceTERXA/Leaderboard
I added a limit to the players cursor and display scorePlusOne instead of score.
Template.leaderboard.helpers({
players: function () {
return Players.find({}, { sort: { score: -1, name: 1 } , limit: Session.get('limit')});
Template.player.helpers({
scorePlusOne: function() {
console.log('scorePlusOne',this.name);
return this.score + 1;
When I increase the limit from 1 to 2, the first player template is not rerendered, but the scorePlusOne helper is rerun.
I don't want any helpers of already-rendered templates to rerun unless they contain reactive data that changes. Is there any way to achieve this behavior?
I don't think you can avoid helpers to be run when reactive data is updated.
However, one way to overrun this could be to use the tracker.autorun() function to wrap a flag to true and allowing your helpers to run or not. It will switch to true as soon your template is re-rendered because of a reactive element.
It would look like this:
Template.leaderboard.rendered = function() {
this.autorun(function(){
flag = true
});
};
And your helper would look like this:
Template.leaderboard.helpers({
players: function () {
if (flag===true){
return Players.find({}, { sort: { score: -1, name: 1 } , limit:
Session.get('limit')});
}
},
selectedName: function () {
if (flag===true){
var player = Players.findOne(Session.get("selectedPlayer"));
return player && player.name;
}
}
});
Each time you make your change, just set your flag = false and that should do the trick.
Cache template helper results in ReactiveVars and move the helper code to template autoruns:
https://github.com/meteor/meteor/issues/4960#issuecomment-132715347
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';
}
}
});
The current method I'm using is to filter a collection, which returns an array, and use
collection.reset(array)
to re-populate it. However, this modifies the original collection, so I added an array called "originalCollectionArray" which keeps track of the initial array state of the collection. When no filtering is active I simply use
collection.reset(originalCollectionArray)
But then, I need to keep track of adding and removing models from the real collection, so I did this:
// inside collection
initialize: function(params){
this.originalCollectionArray = params;
this.on('add', this.addInOriginal, this);
this.on('remove', this.removeInOriginal, this);
},
addInOriginal: function(model){
this.originalCollectionArray.push(model.attributes);
},
removeInOriginal: function(model){
this.originalTasks = _(this.originalTasks).reject(function(val){
return val.id == model.get('id');
});
},
filterBy: function(params){
this.reset(this.originalCollectionArray, {silent: true});
var filteredColl = this.filter(function(item){
// filter code...
});
this.reset(filteredColl);
}
This is quickly becoming cumbersome as I try to implement other tricks related to the manipulation of the collection, such as sorting. And frankly, my code looks a bit hacky. Is there an elegant way of doing this?
Thanks
You could create a collection as a property of the main collection reflecting the state of the filters:
var C = Backbone.Collection.extend({
initialize: function (models) {
this.filtered = new Backbone.Collection(models);
this.on('add', this.refilter);
this.on('remove', this.refilter);
},
filterBy: function (params){
var filteredColl = this.filter(function(item){
// ...
});
this.filtered.params = params;
this.filtered.reset(filteredColl);
},
refilter: function() {
this.filterBy(this.filtered.params);
}
});
The parent collection keeps its models whatever filters you applied, and you bind to the filtered collection to know when a change has occurred. Binding internally on the add and remove events lets you reapply the filter. See
http://jsfiddle.net/dQr7X/ for a demo.
The major problem on your code is that you are using a raw array as original, instead of a Collection. My code is close to the yours but use only Collections, so methods like add, remove and filter works on the original:
var OriginalCollection = Backbone.Collection.extend({
});
var FilteredCollection = Backbone.Collection.extend({
initialize: function(originalCol){
this.originalCol = originalCol;
this.on('add', this.addInOriginal, this);
this.on('remove', this.removeInOriginal, this);
},
addInOriginal: function(model){
this.originalCol.add(model);
},
removeInOriginal: function(model){
this.originalCol.remove(model);
},
filterBy: function(params){
var filteredColl = this.originalCol.filter(function(item){
// filter code...
});
this.reset(filteredColl);
}
});