I've run into an interesting possible bug, but in this case it may be caused by the lack of a function for me to use to delete a document when observing a collection. Or I am misusing observe... which could very well be the case!
Here is sample code that will reproduce the issue I'm having.
I'm using the devel branch as of this writing, so I'm not sure if this works in 0.3.5
observebug.html
<head>
<title>observebug</title>
</head>
<body>
{{> main}}
</body>
<template name="main">
<h1>Example showing possible bug in Meteor wrt observe</h1>
<div>
<p>Try to delete a note. You will notice that it doesn't appear to get deleted. However, on the server, the delete did occur. Refresh the page to see that the delete did in fact occur.</p>
<h2>Notes:</h2>
<ul>
{{#each notes}}
{{> note_row}}
{{/each}}
</ul>
</div>
</template>
<template name="note_row">
<li>{{title}} <button name="delete">delete</button></li>
</template>
observebug.js
// CLIENT
if (Meteor.is_client) {
Notes = new Meteor.Collection("notes_collection");
Meteor.autosubscribe(function () {
Meteor.subscribe("notes_subscription");
});
Template.main.notes = function () {
return Notes.find();
};
Template.note_row.events = {
"click button[name='delete']": function (evt) {
Meteor.call("deleteNote", this._id, function (error, result) {
if (!error) {
console.log("Note deletion successful.");
} else {
console.log("Error when deleting note.");
}
});
}
};
}
// SERVER
if (Meteor.is_server) {
Notes = new Meteor.Collection("notes_collection");
Meteor.methods({
"deleteNote": function (note_id) {
try {
Notes.remove(note_id);
return true;
} catch (e) {
return false;
}
}
});
Meteor.publish("notes_subscription", function () {
var notes = Notes.find({}, {sort: {title: 1}});
var self = this;
// What we're doing here is basically making an exact duplicate
// of the notes collection. A possible use-case for this would be
// to actually make changes to the copied collection on the fly,
// such as adding new fields without affecting the original
// collection.
var upsertHandler = function (note, idx) {
note.some_new_field = 100;
self.set("notes_collection", note._id, note);
self.flush();
};
var handle = notes.observe({
added: upsertHandler,
changed: upsertHandler,
removed: function (note, idx) {
// As far as I can tell, unset won't remove the document,
// only attributes of the document. I don't think there's
// a method to handle actually removing a whole document?
self.unset("notes_collection", note._id);
self.flush();
}
});
self.onStop(function () {
handle.stop();
self.flush();
});
});
// Add example notes
Meteor.startup(function () {
if (Notes.find().count() === 0) {
Notes.insert({title: "Note #1"});
Notes.insert({title: "Note #2"});
Notes.insert({title: "Note #3"});
Notes.insert({title: "Note #4"});
Notes.insert({title: "Note #5"});
Notes.insert({title: "Note #6"});
}
});
}
What you'll be seeing
When you start this application up, you'll see 6 example "notes", each with a delete button. I suggest having your console open so you can see the console.logs. Click the delete button on any note. The note does get deleted, however this doesn't get reflected back up to the client.
I suspect the issue lies in how I'm leveraging observe to create a copy of the collection (which I can then manipulate without affecting the original collection). I feel like I need a function that deletes a whole document, not just some attributes (unset).
EDIT: See the example in action over at http://observebug.meteor.com/
So I've got it figured out. I did indeed need to use observe, and there is no Meteor bug on that end. Just a lack of understanding of what was involved in what I was trying to accomplish. Luckily I found a great starting point within the Meteor code itself and wrote an adjusted version of the _publishCursor function, which I've called publishModifiedCursor.
Here are the adjusted project templates and code:
observe.html
<head>
<title>observe</title>
</head>
<body>
{{> main}}
</body>
<template name="main">
<h1>Example in trying to publish a modified copy of a collection</h1>
<div>
<h2>Notes:</h2>
<ul>
{{#each notes}}
{{> note_row}}
{{/each}}
</ul>
<p><button name="add">add note</button></p>
</div>
</template>
<template name="note_row">
<li>
<strong>Original title:</strong> <em>{{title}}</em><br />
<strong>Modified title:</strong> <em>{{__modified_title}}</em><br />
<button name="delete">delete</button>
</li>
</template>
observe.js
// CLIENT
if (Meteor.is_client) {
ModifiedNotes = new Meteor.Collection("modified_notes_collection");
Meteor.autosubscribe(function () {
Meteor.subscribe("modified_notes_subscription");
});
Template.main.notes = function () {
return ModifiedNotes.find();
};
Template.main.events = {
"click button[name='add']": function (evt) {
Meteor.call("addNote", function (error, result) {
if (!error) {
console.log("Note addition successful.");
} else {
console.log("Error when adding note.");
}
});
}
};
Template.note_row.events = {
"click button[name='delete']": function (evt) {
Meteor.call("deleteNote", this._id, function (error, result) {
if (!error) {
console.log("Note deletion successful.");
} else {
console.log("Error when deleting note.");
}
});
}
};
}
// SERVER
if (Meteor.is_server) {
Notes = new Meteor.Collection("notes_collection");
Meteor.methods({
"addNote": function () {
try {
Notes.insert({title: "Note #" + (Notes.find().count() + 1)});
return true;
} catch (e) {
return false;
}
},
"deleteNote": function (note_id) {
try {
Notes.remove(note_id);
return true;
} catch (e) {
return false;
}
}
});
Meteor.publish("modified_notes_subscription", function () {
// Pull up the original notes_collection
var notes = Notes.find({}, {sort: {title: 1}});
// Publish a near identical collection, with modifications
this.publishModifiedCursor(notes, "modified_notes_collection", function (note) {
note.__modified_title = getTitleModifiedByServer(note.title);
return note;
});
});
var getTitleModifiedByServer = function (title) {
return title + "!!!";
};
// Add example notes
Meteor.startup(function () {
if (Notes.find().count() === 0) {
Notes.insert({title: "Note #1"});
Notes.insert({title: "Note #2"});
Notes.insert({title: "Note #3"});
Notes.insert({title: "Note #4"});
Notes.insert({title: "Note #5"});
Notes.insert({title: "Note #6"});
}
});
_.extend(Meteor._LivedataSubscription.prototype, {
publishModifiedCursor: function (cursor, name, map_callback) {
var self = this;
var collection = name || cursor.collection_name;
var observe_handle = cursor.observe({
added: function (obj) {
obj = map_callback.call(self, obj);
self.set(collection, obj._id, obj);
self.flush();
},
changed: function (obj, old_idx, old_obj) {
var set = {};
obj = map_callback.call(self, obj);
_.each(obj, function (v, k) {
if (!_.isEqual(v, old_obj[k])) {
set[k] = v;
}
});
self.set(collection, obj._id, set);
var dead_keys = _.difference(_.keys(old_obj), _.keys(obj));
self.unset(collection, obj._id, dead_keys);
self.flush();
},
removed: function (old_obj, old_idx) {
old_obj = map_callback.call(self, old_obj);
self.unset(collection, old_obj._id, _.keys(old_obj));
self.flush();
}
});
self.complete();
self.flush();
self.onStop(_.bind(observe_handle.stop, observe_handle));
}
});
}
See it live over at http://observebug.meteor.com/. The final effect is underwhelming, since it's just a test involving adding/removing notes... but hey now it works!
Hope this helps someone else out in the future.
this.unset requires three parameters, the first and second one are correct; it however requires a third parameter to tell which attributes to unset. This is a bug in your code and not in Meteor.
However, note that when your callback is called tha the document is already deleted from the collection and that you are just working on a copy of the object. Exactly, Notes.find({}).count() reveals the remaining count at any point during your callback.
// I don't think there's
// a method to handle actually removing a whole document?
If you want two collections, create two collections. By only keeping one and trying to do all sorts of magic, you are calling functions that are not even meant to be doing what you want them to do. If you just create
Notes_copy = new Meteor.Collection("notes_collection_copy");
and use that to keep track of temporary notes, you won't run into any of the trouble you had now.
Related
How to publish single objects seems not clear enough to me. Please what's the best way to handle this. This code snippet does not display anything on the view.
Helper file
singleSchool: function () {
if (Meteor.userId()) {
let myslug = FlowRouter.getParam('myslug');
var subValues = Meteor.subscribe('SingleSchool', myslug );
if (myslug ) {
let Schools = SchoolDb.findOne({slug: myslug});
if (Schools && subValues.ready()) {
return Schools;
}
}
}
},
Publish file
Meteor.publish('SingleSchool', function (schoolSlug) {
check( schoolSlug, Match.OneOf( String, null, undefined ) );
user = Meteor.users.findOne({_id:this.userId})
if(user) {
if(user.emails[0].verified) {
return SchoolDb.findOne({ slug: schoolSlug, userId: {$lt: this.userId}});
} else {
throw new Meteor.Error('Not authorized');
return false;
}
}
});
template file
<template name="view">
{{#if currentUser}}
{{#if Template.subscriptionsReady }}
{{#with singleSchool}}
{{singleSchool._id}}
{{singleSchool.addschoolname}}
{{/with}}
{{/if}}
{{/if}}
</template>
As you said "This code snippet does not display anything on the view." well, inside Meteor.publish you need to return cursor, not array or any other object.
So use this code:
Meteor.publish('SingleSchool', function (schoolSlug) {
check( schoolSlug, Match.OneOf( String, null, undefined ) );
var user = Meteor.users.findOne({_id:this.userId});
if(!user || !user.emails[0].verified) {
throw new Meteor.Error('Not authorized');
}
return SchoolDb.find({ slug: schoolSlug, userId: {$lt: this.userId}},{limit:1});
});
I would definitely recommend you to go through How to avoid Common Mistakes
When I am concerned only for a single object, I implement this using a meteor method:
Meteor.methods({
"getSingleSchool":function(schoolSlug) {
//... check args and user permissions
return SchoolDb.findOne({ slug: schoolSlug, userId: {$lt: this.userId}});
},
});
Then in the template I run this method in the onCreated autorun part:
Template.view.onCreated(function(){
const instance = this;
instance.state = new ReactiveDict();
instance.autorun(function(){
let my slug = FlowRouter.getParam('myslug');
// load if no loaded yet
if (my slug && !instance.state.get("singleSchool")) {
Meteor.call("getSingleSchool", mySlug, function(err, res){
//handle err if occurred...
this.state.set("singleSchool", res);
}.bind(instance)); //make instance available
}
});
});
The helper then just returns a value, if the school is loaded:
singleSchool: function () {
return Template.instance().state.get("singleSchool");
},
why is this an endless loop? [ Iron Router + Fast Render + Blaze]
Router.route("/:cycle_uri", {
name: "cycle"
,template: "home"
,onBeforeAction: function () {
console.log("is there a loop here?") // this is what confirms that it's a continuous loop
var cycle = Cycles.findOne({
"uri": this.params.cycle_uri
});
if (typeof cycle === "undefined") {
this.render("notFound"); return;
} else {
ActiveCycle.set(cycle); // if I remove this, there is no continuous loop anymore... but if I remove it I don't see how to have this info in the client
this.render("home");
}
}
,waitOn: function () {
Meteor.subscribe('featuredListsPub', {
'program_id': this.params.cycle_uri
});
}
,fastRender: true
});
I was trying to update ActiveCycle variable so I can read it in the frontend but it's not actually working... I'm certainly doing something wrong, but would like to first understand why updating the reactive var is creating a loop.
I've also tried
if (ActiveCycle.get() !== cycle) {
ActiveCycle.set(cycle);
}
but it also enters a loop... which I don't understand why
for your question in the comments:
How do you subscribe to two publications:
here is my answer:
waitOn: function () {
return [
Meteor.subscribe('subscription1'), Meteor.subscribe('subscription2')
];
}
However, i strongly recommend:
Create on publication and return two cursors
Use Template level subscriptions
Good Luck!
An example of Template level subscriptions:
Template.templatename.onCreated(function () {
Template.autorun(function () {
var subscription = Meteor.subscribe('some_publication');
if (subscription.ready()) {
// do something
}
});
});
and within the template
<template name="templatename">
{{#if Template.subscriptionsReady}}
<div>Your Template here...</div>
{{else}}
<p>Loading...</p>
{{/if}}
</template>
A nice article is right here.
I'm new to Meteor.
Trying to render items from collection but Meteor.renderList(observable, docFunc, [elseFunc]) alway go to elseFunc.
this.ComponentViewOrdersFlow = Backbone.View.extend({
template: null,
initialize: function() {
var frag;
Template.ordersFlow.events = {
"click a": function(e) {
return App.router.aReplace(e);
}
};
this.template = Meteor.render(function() {
return Template.ordersFlow();
});
console.log(Colors);
frag = Meteor.renderList(
Colors.find(),
function(color) {
console.log(color);
},
function() {
console.log('else consdition');
}
);
},
render: function() {
this.$el.html(this.template);
return this;
}
});
Initially I thought that Collection is empty, but console.log(Colors) shows that there are items in collection. Moreover if I use Meteor.render(... -> Template.colors({colors: Colors.find()}) ) it renders template end show Collection items there.
Meteor version 0.6.6.3 (Windows 7, 64bit)
Mongo - connected to MongoLab
Thank you for any help.
Jev.
Can't really explain this well in the comments, so here is a very, very simple example of using the Meteor template engine. This is a 100% functional app, showcasing basic reactivity. Note that I never call render() or renderList() anywhere.
All this app does is show a button, that when clicked, adds a number to a list. The number is reactively added to the list, even though I never do anything to make that reactivity explicit. Meteor's templates are automatically reactive! Try it out - this is all of the code.
numbers.html:
<body>
{{> numberList}}
</body>
<template name="numberList">
<ul>
{{#each numbers}}
<li>{{number}}</li>
{{/each}}
</ul>
<button>Click Me</button>
</template>
numbers.js:
var Numbers = new Meteor.Collection("numbers");
if (Meteor.isClient) {
Template.numberList.numbers = function() {
return Numbers.find();
};
var idx = 0;
Template.numberList.events({
"click button": function() {
Numbers.insert({
number: idx
});
idx++;
}
});
}
UPDATED
NOW I try to do this in my app (thx to Akshat)
//common
LANG = 'ru';
Dictionary = new Meteor.Collection("dictionary");
//if server
Meteor.startup(function () {
if (Dictionary.find().count() === 0) {
// code to fill the Dictionary
}
});
Meteor.publish('dictionary', function () {
return Dictionary.find();
});
//endif
//client
t = function(text) {
if (typeof Dictionary === 'undefined') return text;
var res = Dictionary.find({o:text}).fetch()[0];
return res && res.t;
}
Meteor.subscribe('dictionary', function(){
document.title = t('Let the game starts!');
});
Template.help.text = t('How to play');
//html
<body>
{{> help}}
</body>
<template name="help">
{{text}}
</template>
Still doesn't work as we wanted: when templates are rendered Dictionary was undefined. Butt('How to play') in console works perfectly )
Javascript variables aren't shared between the client and server reactively. You have to use a Meteor Collection to store your data e.g
if (Meteor.isServer) {
var Dictionary = new Meteor.Collection("dictionary");
if(Dictionary.find().count() == 0) {
//If the 'dictionary collection is empty (count ==0) then add stuff in
_.each(Assets.getText(LANG+".txt").split(/\r?\n/), function (line) {
// Skip comment lines
if (line.indexOf("//") !== 0) {
var split = line.split(/ = /);
DICTIONARY.insert({o: split[0], t:split[1]});
}
});
}
}
if (Meteor.isClient) {
var Dictionary = new Meteor.Collection("dictionary");
Template.help.text = function() {
return Dictionary.find({o:'Let the game starts!'});
}
}
In the above i'm assuming you have the autopublish package in (its in by default when you create a package so this shouldn't really bother you, but just in case you removed)
With your document title you would have to use a slightly different implementation because remember the data wouldn't be downloaded at the time Meteor.startup is run, since the html and javascript are downloaded first & the data is empty, then the data comes in slowly (and then reactively fills the data up)
I'm new to Meteor and barely understand any of it but let's say I have a collection called mycollection, declared way up top so it's available in both the client and server section:
mycollection = new Meteor.Collection('MyDumbCollection');
And then I have something like this:
if (Meteor.isClient) {
Deps.autorun(function() {
Meteor.subscribe('mycollectionSubscription');
});
Template.myshittytemplate.rendered = function() {
$("#heynow").prepend(shitfuck).dosomething();
godammit = thisfuckingthing;
//paraphrasing
mycollection.insert({thing1: thisfuckingthing});
};
}
if (Meteor.isServer) {
Meteor.publish('mycollectionSubscription', function () {
return mycollection.find();
});
};
And then in my template:
<template name="myshittytemplate">
<div id ="heynow">
{{#each mycollection}}
{{{thing1}}}
{{/each}}
</div>
</template>
What I'm trying to do is have the 'thisfuckingthing' html that's created in the #heynow div saved to the collection and published to everybody. If there's a way to make Meteor simply observe changes to the dom and save them, that's even better.
I do have autopublish uninstalled, if that makes a difference. Halp.
In client Template
Template.myshittytemplate.mycollection = function() {
return mycollection.find({}).fetch();
};
Template.myshittytemplate.rendered = function() {
$(function(){
$("#heynow").prepend(shitfuck).dosomething();
godammit = thisfuckingthing;
//paraphrasing
mycollection.insert({thing1: thisfuckingthing},function(err,_id){
if(err){
console.log(err);
return;
});
console.log(_id);
});
};
}
I needed this in the Client part:
Template.myshittytemplate.mycollection = function() {
return mycollection.find();
};
Hopes this helps somebody!