ref: this jsfiddle
(I don't know if this is a Ractive question or a plain-old-javascript question)
I have a Ractive model, "item":
window.item = new Ractive({
el: "#container",
template: "<p>words</p>",
oninit: function() { window.little_ractive.set({ 'thing_to_do': this.do_this, 'item': this }); },
do_this: function() { this.say("yeah baby"); },
say: function(what) { console.log(what); }
});
Notice that the model initializes two attributes in another Ractive model, which looks like this:
window.little_ractive = new Ractive({
finish: function() { this.get('thing_to_do').apply(this.get('item')); }
});
We can observe that little_ractive is properly initialized with attributes pointing to the item model and its "do_this" method.
If we now execute:
little_ractive.finish()
The context ('this') is incorrectly established in the "do_this" method, and it returns an error "this.say() is not a function"
How can the methods of one Ractive model be executed from another?
update: The jsfiddle uses Ractive version 1.0.0 build9. Ractive version 0.7.3 does not exhibit this problem, it works as one would expect.
update: The unexpected behaviour first appeared in version 0.8.0
Any functions that are stored at the root of a ractive instance's data are automatically bound to the instance e.g. ractive.set('foo', function () { console.log('this will always be ractive'); }). This is so that calling root functions execute as expected in templates.
If you move functions that don't need to be bound one level deep e.g. ractive.set('fns.foo', function () { console.log('this is not bound'); }), then you can pass them around and call/apply them as needed. fiddle
Related
I use a bunch of helper methods in my project. Some of them require to load the whole collection into the client due the restriction of the api on client side (the distinct function!). I googled the problem and found Meteor.methods as solution.
Can I use helpers (like this.helpers) into Meteor methods? Or how should I dynamically update my data in the frontend?
Can someone give me an example?
Additional information:
class View2 {
constructor($interval, $scope, $reactive) {
'ngInject';
$reactive(this).attach($scope);
this.helpers({
getOrderNumber(){
this.tempVar = Kafkadata.find().fetch();
this.tempVar2 = _.pluck(this.tempVar, 'orderNumber');
this.tempVar3 = _.uniq(tempVar2, false);
return this.tempVar3;
},
});
}
This is an example for a helpers query. Currently, this code runs client-side. I get ALL orders(tempvar) and then remove ALL data except the ordernumbers(tempvar2). At the end I remove all multiple ordernumbers. ordernumber is not an unique value. Here is an example from one of the collections:
{"orderNumber":"f2a3ed95-fcc3-4da0-9b3f-32cf5ed087f8","value":12480,"booleanValue":false,"intValue":12480,"doubleValue":0,"status":"GOOD","itemName":"MILLING_SPEED","timestamp":1479145734448,"_id":{"_str":"5824f4bc7ff3f0199861f11d"}}
I want to use functions like db.collection.distinct(). But they only work server-side. I think I must use Meteor.methods()to make this thing server-side. But what about this helpers function? How do they work on Meteor.methods()?
EDIT2:
my test:
client-side:
folder:myProject/imports/ui/view1
class View1 {
constructor($interval, $scope, $reactive) {
'ngInject';
$reactive(this).attach($scope);
this.helpers({
// some code
getTestData(){
Meteor.call('allTestData',function(error, result){
if(error){
console.log("error");
}else{
return result;
}
});
}
}); //end of contructor
// this is my testfunction, which is bound to a button!
testFunction(){
Meteor.call('allTestData',function(error, result){
if(error){
alert('Error');
}else{
console.log(result);
}
});
}
on the server-side:
folder:myProject/server/main.js
Meteor.methods({
allTestData:()=>{
var results=Kafkadata.find().count();
console.log(results);
return results;
},
});
and this is my view1.html:
//some code
<md-button ng-click="view1.testFunction()"> It works!</md-button>
<h1>{{view1.getTestData}}</h1>
Why does the button work, but not the helper?
Even though .distinct is supported by Mongo, Meteor does not expose it, even on the server. You just have to use _.uniq as your example shows, but for performance reasons it's better if it runs on the server.
Below is an example of a helper that I use:
aweek: () => {
if (debug)
console.log("Querying weekly appointments for "+this.selectedWeek.format("Do MMMM"));
var weekApts = Appointments.find(
{start: {$gte: new Date(this.getReactively('this.selectedWeek').clone().day(1)),
$lt: new Date(this.getReactively('this.selectedWeek').clone().endOf('week'))},
elderid: Meteor.userId()
}).fetch();
return utils.services.getAWeek(weekApts,utils.data.elderTimeFormat);
},
Note the use of this.getReactively('this.selectedWeek') in the code... basically this tells Meteor to run this helper reactively, so if the value of this.selectedWeek changes, the helper will get re-run. So when I click on a week in the calendar and update the variable, it runs my helper again to get the data.
The utils.services.getAWeek() function does some calculation and formatting on the array of data that makes is easier to display.
If you create a Meteor Method to do processing, I would make it update a collection with its results, and then your helper on the client will update automatically. Best make the technology do the work for you :)
Clearly, I am doing something wrong with ReactiveVar because I cannot get it to work as I expect it should.
I am trying to set the value of an ReactiveVar by calling a Meteor.call method which returns the list of usernames. But it does not update when the usernames get changed in another part of the app.
I tried both:
Template.qastatistics.created = function () {
this.trackUsernames = new ReactiveVar(false);
var instance = Template.instance();
Meteor.call('trackUsernames', function (err, data) {
instance.trackUsernames.set(data);
});
};
and:
Template.qastatistics.helpers({
users: function () {
var usernames,
instance = Template.instance();
if (instance.trackUsernames.get() === false) {
Meteor.call('trackUsernames', function (err, data) {
instance.trackUsernames.set(data);
});
}
usernames = instance.trackUsernames.get();
...
But neither updates the list of usernames when these change in the database.
Is this even possible with ReactiveVars or have I completely misunderstood them?
EDIT: The usernames I mention are not from Meteor.users collection, but rather a distinct call from another collection that has usernames in it.
Fist of all I would use the onCreated function instead of defining created. That's a little more extendable and it's the new API. created is just kept around for backwards compatibility.
About your problem. You are right, you seem to have misunderstood what ReactiveVars do. They are a reactive data source. That means that when you call myReactiveVar.get in some Tracker.autorun (aka. reactive computation), the computation will rerun whenever myReactiveVar.set is called.
You got the first part right. Spacebars helpers always run inside their own computation. What you got wrong is thinking that a method call is a reactive action. That means, that you could call trackUsernames and set the trackUsernames ReativeVar again and the value in your template would update itself. But a method is only run once. It doesn't do anything fancy with reactivity.
A method call only transfers data once. When you publish a set of documents (like all users) on the other hand, they will be updated dynamically. Whenever a change happens inside that set of published documents, it will be synced to the client. So in general, it's a better idea to use publications and subscriptions to sync data reactively. If you'd want to use a method for the same thing you'd need to do some kind of polling (so your back in the stone-age again).
The easiest way to implement what you are trying to do is to use Meteor.users.find().fetch(). As it says in the docs fetch registers dependencies for all the documents you are fetching if it's being called from within a reactive computation.
First you'll need to properly set up your publications, so that users can see other users usernames. I'll leave that to you. Then you need to reimplement your helper
Template.qastatistics.helpers({
users: function () {
var usernames = _.pluck(Meteor.users.find().fetch(), 'username');
...
Thanks to suggestions from #kyll, I managed to get what I wanted by publishing the data I need:
server:
cope.publish.usernamesID = Random.id();
Meteor.publish("itemsusernames", function () {
self = this;
var initializing = true;
var handle = Items.find().observeChanges({
added: function (id) {
!initializing && self.changed(
"itemsusernames",
cope.publish.usernamesID,
Items.distinct("p4User"));
},
changed: function (id) {
!initializing && self.changed(
"itemsusernames",
cope.publish.usernamesID,
Items.distinct("p4User"));
},
removed: function (id) {
!initializing && self.changed(
"itemsusernames",
cope.publish.usernamesID,
Items.distinct("p4User"));
}
});
initializing = false;
self.added("itemsusernames", cope.publish.usernamesID, Items.distinct("p4User"));
self.ready();
self.onStop(function () {
handle.stop();
});
});
client:
users: function () {
var usernames = [],
oUsernames = ItemsUsernames.find().fetch();
if (!oUsernames[0]) return [];
usernames = $.map(oUsernames[0], function (value, index) {
if (!isNaN(index)) {
return [value];
}
});
...
And ofcourse: ItemsUsernames = new Mongo.Collection("itemsusernames");
I have a helper function that depends on a collection document lookup, the result of which it passes to a subscription via a Session. It then needs to query the documents from that subscription.
The code explains it better than I could.
Helper:
var selection = Selections.findOne()
var itemIds = function() {
return selection && selection.itemIds
}
var itemIdsArray = itemIds()
Session.set('itemIdsArray', itemIdsArray)
console.log(Session.get('itemIdsArray'))
_.each(Items.find({_id: {$in: itemIdsArray}}).fetch(), function(element, index, list) {
//doing stuff
})
Subscription:
Meteor.subscribe('itemsById', Session.get('itemIdsArray'))
Publication:
Meteor.publish('itemsById', function(itemIdsArray) {
return Items.find({_id: {$in: itemIdsArray}})
})
My console.log returns an undefined value before it returns the array of IDs. So undefined gets passed all the way to the publication, which complains of a null value (which is weird in itself) after $in and breaks.
My solution was to set the Session to default to [],
Session.setDefault(`itemIdsArray`, [])
which I honestly had high hopes that it'd work, but alas, it did not.
I've tried putting it inside IronRouter's onBeforeAction, I've tried putting it at the top of the helper, I've tried putting it pretty much anywhere but it still logs and returns undefined once before it gets the correct value.
I've also tried to move around my subscription, from waitOn to subscriptions to onAfterAction to onRendered, but those attempts have been utterly fruitless.
What should I do?
That's fairly typical behavior in Meteor. Session variables are not always ready at the time. The usual way of dealing with this is to introduce a guard in the helper that checks the variable is defined before doing anything else with it.
In your case something like this would work: itemsIdArray = itemIds() || [];
To answer the actual question you are asking, where do you set Session defaults that they are available in your subscriptions: it's not important where you set them, but when you access them. You can wait for the subscription to be ready using iron router's waitOn() function, or you can check the subscription handle's ready() function (see https://github.com/oortcloud/unofficial-meteor-faq#user-content-how-do-i-know-when-my-subscription-is-ready-and-not-still-loading)
If you return a subscription in your waitOn option of Iron Router you should have the data in your template then:
Router.route('/yourRoutePath/:_id', {
// this template will be rendered until the subscriptions are ready
loadingTemplate: 'loading',
waitOn: function () {
// return one handle, a function, or an array
return Meteor.subscribe('itemsById', this.params._id);
},
action: function () {
this.render('myTemplate');
}
});
Your template helper:
Template.myTemplate.helpers({
items: function() {
return Items.find();
}
});
I noticed that you publish Items collection and you want to use Selections collection in your helper. If you need more than one subscription, you can return an array of subscriptions in waitOn:
waitOn: function () {
// return one handle, a function, or an array
return [
Meteor.subscribe('itemsById', this.params._id),
Meteor.subscribe('selections')
];
}
WaitOn ensures that your template will be rendered when all subscriptions are ready.
I'm trying to use a client side collection as a site configuration system. I insert documents representing my different pages, and the iron-router and navigation tabs all use them to determine what pages they are and what templates are represented by them. Each page uses a {{> contentTemplate}} inclusion helper to load it's relevant template.
It all works great, when the data has all loaded. When I restart the app on certain pages, the data hasn't loaded yet, and I receive the Exception from Deps recompute function: Error: Expected null or template in return value from inclusion function, found: undefined error.
Here's my javascript:
StoriesArray = [
{ category: 'teaching', contentTemplate: 'teachingHome', title: 'Teaching Home'},
...
];
Stories = new Meteor.Collection(null);
StoriesArray.forEach(function (story, index) {
story._id = index + '';
Stories.insert(story);
});
// in main.js
Template.teachingPost.contentTemplate = function() {
console.log(this);
console.log(this.contentTemplate);
return Template[this.contentTemplate];
};
// in router.js
this.route('teaching', {
layoutTemplate: 'teachingPost',
data: function() { return Stories.findOne({contentTemplate: 'teachingHome', category: 'teaching'}); }
});
The console logs in the contentTemplate helper above log twice, the first time as this:
Object {} main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:45
undefined main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:46
and the second time as this:
Object {category: "teaching", contentTemplate: "teachingHome", title: "Teaching Home"} main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:45
teachingHome main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:46
so the router is simply trying to load this data too early.
I've tried putting the StoriesArray loading process into different files all over my app, including lib, and even tried putting it into Meteor.startup, but it's always the same result.
The normal iron-router waitOn/subscription pattern doesn't really apply here, since this is a client side collection built with null, that has no server representation. I don't want this to have server representation, because this is static content that there's no need to go to my server for.
How do I ensure this information is done before continuing?
Untested, but per Iron Router's docs on waitOn:
Returning a subscription handle, or anything with a ready method from the waitOn function will add the handle to a wait list.
Also in general it's better to use find with data, rather than findOne, as find will return an empty cursor when the collection is empty as opposed to findOne returning undefined. So try this:
// in router.js
this.route('teaching', {
layoutTemplate: 'teachingPost',
data: function() {
return Stories.find({contentTemplate: 'teachingHome', category: 'teaching'});
},
waitOn: function() {
var handle = {};
handle.ready = function() {
if (Stories.find().count() !== 0)
return true;
else
return false;
}
return handle;
}
});
And adjust your Template.teachingPost.contentTemplate function to work with a cursor rather than an object.
I have a publication whose scope depends on a element property from another collection. Basically it looks like this on the server:
Meteor.publish('kids', function (parent) {
return Kids.find({ _id: { $in: parent.childrenIds } });
}
In the example above parent.childrenIds is an array containing the _id's of all the kids that are children of the parent. This works fine until I want to add a new child to the parent:
newKidId = Kids.insert({ name: 'Joe' });
Parents.update({ _id: parentId }, { $push: { childrenIds: newKidId } });
This works on the server for the Kids collection (i.e., the new kid is added) and it updates the parent's childrenIds array with the newKidId. BUT it does not update the above 'kids' publication (the cursor is not updated/modified). As a result, the client's Kids collection is not updated (and it looks like the change to Kids is rolled back on the client).
When the client refreshes, all publications are stopped/restarted and the new kid (Joe) is finally published to the client.
Is there a way to avoid refreshing the client and forcing the re-publication of the Kids collection (ideally only sending the new kid Joe to the client)?
One of the things that is often misunderstood in Meteor is that there is no reactivity on the server. Dynamic descriptions need to be handled by Deps.autorun blocks on the client. To do this, first make sure you are not including the autopublish package by using this command in project directory:
$ meteor remove autopublish
Second, set up an autorun block on the client like:
Meteor.startup(function(){
Meteor.subscribe('parents');
Deps.autorun(function() {
parent = Parents.findOne();
if (!parent) return;
Meteor.subscribe('kids', parent);
});
});
This will tear down and set up subscriptions as the parent object changes.
You can see a full working example at https://gist.github.com/jagill/5473599 .
In meantime there were quite some packages made to deal with reactive publish functions. I am an author of meteor-related, and at the end of the package's README I compare my package with few other packages:
meteor-reactive-publish
meteor-publish-with-relations
meteor-smart-publish
reywood:publish-composite
copleyjk:simple-publish
These days you can simply use reactive-publish package (I am one of authors):
Meteor.publish('kids', function (parentId) {
this.autorun(function (computation) {
var parent = Parents.findOne(parentId, {fields: {childrenIds: 1}});
return Kids.find({_id: {$in: parent.childrenIds}});
});
}
It is important to limit Parents' query fields only to childrenIds so that autorun does not rerun for any other changes to Parents document.
I think you need to use observe in the publish function if a published query relies on a second query. Deps.autorun on the client is not necessary.
See discussions on Meteor server reactivity and Reactive updates when query is filtered by another query.
This is some code based on http://docs.meteor.com 'counts-by-room' example.
Meteor.publish( "kids", function(parent_id){
var self = this;
Parents.find({_id: parent_id}, { childrenIds: 1 }).observe({
added: function (document){
document.childrenIds.forEach( function(kidId){
self.added("kids", kidId, Kids.findOne( { _id: kidId}, {name: 1, _id: 1} ));
});
},
removed: function (oldDocument){
oldDocument.childrenIds.forEach( function(kidId){
self.removed("kids", kidId, Kids.findOne( { _id: kidId}, {name: 1, _id: 1} ));
});
},
changed: function (newDocument, oldDocument){
var oldLength = oldDocument.childrenIds.length;
var newLength = newDocument.childrenIds.length;
if (newLength > oldLength){
self.added("kids",
newDocument.childrenIds[newLength-1],
Kids.findOne( { _id: newDocument.childrenIds[newLength-1] }, {name:1, _id:1}) );
}
else{
self.removed("kids",
oldDocument.childrenIds[oldLength-1],
Kids.findOne( { _id: oldDocument.childrenIds[oldLength-1] }, {name:1, _id:1}) );
}
}
});
self.ready();
});