Writing user entered text (input) back into it's document (in mongo) - meteor

I have a meteor template rendering a document comming out of mongo db.
Parts of the document are editable (they render as html input elements).
Now I need the data to flow back into the document (and into mongo),
What is the best way to do this ?
The answer is easy if I want to write back the value of doc.a :
doc = {a: "hello"}
it is less easy with : doc.a[0].z
doc = {a: [{z: "hello"}]}
because in order to do the update, the path must be remembered
in order to write the update statement.
Updating the whole document whenever a field changes looks simple,
but inefficient...
It is an extremely common use case, some frameworks (EmberJs) have
magical bindings that modifies the model whenever the widget's value
changes.
How is this done in meteor ?

As you point out, it would probably be inefficient to run a db update command whenever the input changes. This is especially true for draggable elements like sliders.
One thing you can do is separate the db query into a function, and then debounce it using underscore.js, like so (untested):
var debouncedUpdate = _.debounce(function(newObject) {
CollectionName.update({_id: newObject._id},newObject);
},300,true);
Template.objectInput.events({
'keydown #text-input': function(event) {
var newValue = $(this.find('#text-input')).val();
var self = this;
self.value = newValue;
debouncedUpdate(self);
},
});

Related

How to know if Meteor Collection Subscription did not change. [Meteor + Blaze]

Below is the piece of subscription on UI.
Template.AssociateEmp.onCreated(function(){
this.validEmail = new ReactiveVar('');
const tpl = this;
this.autorun(() => {
var email = this.validEmail.get();
this.subscribe('GetUnassociatedUser', email, {
onReady: function () {},
onError: function () {}
});
});
});
Is there a way to know that even if the dynamic data changed (here validEmail), Meteor Subscription was unaffected and did not change its data on UI? Is there any flag or something that triggers when subscription data is unchanged?
Autorun, ReactiveVar and Subscriptions
In your code example the subscription itself will re-run the server's publication function as the subscription's input variable email depends on the reactive variable validEmail and thus triggers the autorun when validEmail changes.
You can easily check that on your server console by logging something to the console within the publication.
If validEmail remains unchanged than there is no reason for autorun to trigger (unless there are other reactive sources that may not be added to your code example).
What about the subscribed data
Now if something has caused the subscription to re-run and you want to know if the data of a collection has been changed you could easily check on collection.count() but this could be flawed.
Imagine your publication is parameterized to include different fields by different parameters, then the data that is transfered to the client side collection will be different.
You would then require a method to check on the client side collection's data integrity.
Use hashes to verify integrity
A possible help would be to generate hases from the dataset using the sha package.
You could for example create one hash of your whole collection:
// get data
const data = Collection.find().fetch();
// map data to strings
// and reduce to one string
const hashInput = data.map(doc => JSON.stringify(doc) ).reduce((a, b) => a + b);
// generate hash
const collectionHash = SHA256(hashInput);
After the next onReady you can generate a new hash of the collection and compare it with the previous hash. If they are different, then something has changed.
This also removes the need for iterating the collection's documents if you only want to know if the data has changed but it won't reveal which document has changed.
Hashing single documents
Hashing single documents gives you more insight about what has changed. To do that you only need to create a map of hashes of your collection:
// get data
const data = Collection.find().fetch();
// map data to strings
const hashes = data.map(doc => { _id: doc._id, hash: SHA256( JSON.stringify(doc) ) });
You can store these hashes together with a document's _id. If the hash of a document is different after a new subscription you can assume that the change is related to this document.
General notes
hashing is some kind of expensive operation so it might be difficult to keep up with performance on large collections
usually you should design your pub / sub and autorun in a way that when the input changes the output changes
code is cold-written so it may not work out of the box. Please let me know if something does not.

Meteor GroundDB granularity for offline/online syncing

Let's say that two users do changes to the same document while offline, but in different sections of the document. If user 2 goes back online after user 1, will the changes made by user 1 be lost?
In my database, each row contains a JS object, and one property of this object is an array. This array is bound to a series of check-boxes on the interface. What I would like is that if two users do changes to those check-boxes, the latest change is kept for each check-box individually, based on the time the when the change was made, not the time when the syncing occurred. Is GroundDB the appropriate tool to achieve this? Is there any mean to add an event handler in which I can add some logic that would be triggered when syncing occurs, and that would take care of the merging ?
The short answer is "yes" none of the ground db versions have conflict resolution since the logic is custom depending on the behaviour of conflict resolution eg. if you want to automate or involve the user.
The old Ground DB simply relied on Meteor's conflict resolution (latest data to the server wins) I'm guessing you can see some issues with that depending on the order of when which client comes online.
Ground db II doesn't have method resume it's more or less just a way to cache data offline. It's observing on an observable source.
I guess you could create a middleware observer for GDB II - one that checks the local data before doing the update and update the client or/and call the server to update the server data. This way you would have a way to handle conflicts.
I think to remember writing some code that supported "deletedAt"/"updatedAt" for some types of conflict handling, but again a conflict handler should be custom for the most part. (opening the door for reusable conflict handlers might be useful)
Especially knowing when data is removed can be tricky if you don't "soft" delete via something like using a "deletedAt" entity.
The "rc" branch is currently grounddb-caching-2016 version "2.0.0-rc.4",
I was thinking about something like:
(mind it's not tested, written directly in SO)
// Create the grounded collection
foo = new Ground.Collection('test');
// Make it observe a source (it's aware of createdAt/updatedAt and
// removedAt entities)
foo.observeSource(bar.find());
bar.find() returns a cursor with a function observe our middleware should do the same. Let's create a createMiddleWare helper for it:
function createMiddleWare(source, middleware) {
const cursor = (typeof (source||{}).observe === 'function') ? source : source.find();
return {
observe: function(observerHandle) {
const sourceObserverHandle = cursor.observe({
added: doc => {
middleware.added.call(observerHandle, doc);
},
updated: (doc, oldDoc) => {
middleware.updated.call(observerHandle, doc, oldDoc);
},
removed: doc => {
middleware.removed.call(observerHandle, doc);
},
});
// Return stop handle
return sourceObserverHandle;
}
};
}
Usage:
foo = new Ground.Collection('test');
foo.observeSource(createMiddleware(bar.find(), {
added: function(doc) {
// just pass it through
this.added(doc);
},
updated: function(doc, oldDoc) {
const fooDoc = foo.findOne(doc._id);
// Example of a simple conflict handler:
if (fooDoc && doc.updatedAt < fooDoc.updatedAt) {
// Seems like the foo doc is newer? lets update the server...
// (we'll just use the regular bar, since thats the meteor
// collection and foo is the grounded data
bar.update(doc._id, fooDoc);
} else {
// pass through
this.updated(doc, oldDoc);
}
},
removed: function(doc) {
// again just pass through for now
this.removed(doc);
}
}));

Applying Meteor collection insert in javascript array for loop

I am new to javascript and very new to Meteor. Is this code correct? I need to define a function which will take an array of values and insert them in a Meteor Collection "FooterButtons"?
Client code
replaceCollectionContents(['NO', 'B', 'YES']);
Both code
replaceCollectionContents = function (buttonsList) {
FooterButtons.remove();
for(i = 0; i < buttonsList.length; i++) {
FooterButtons.insert(buttonsList[i]);
}
};
You cannot directly insert a string to a collection. The insert method expects a document which is of type object.
Try this instead -
FooterButtons.insert({ text: buttonsList[i] });
Also, I notice that you are trying to clear your FooterButtons collection. Please note that you cannot clear a collection like this from the client side as it is considered untrusted code. From client side, you can only remove one document at a time, specified by its _id.
I would recommend you to use a method instead.
Meteor.methods({
replaceCollectionContents: function (buttonsList) {
// remove all existing documents in the collection
FooterButtons.remove({});
// insert new button documents into the collection
buttonsList.forEach(function (button) {
FooterButtons.insert({ text: button });
});
}
});
And call this method when needed
Meteor.call("replaceCollectionContents", ['NO', 'B', 'YES']);
Inside the method, note that I'm passing a {} selector to the remove method because for safety reasons, Meteor does not remove any documents if selector is omitted.
You can read more about remove in Meteor docs.
If I understand correctly, you need to seed data into FooterButtons collection, correct?
Put this code somewhere on your server folder:
buttonsList = ['NO', 'B', 'YES'];
if (FooterButtons.find().count() === 0) {
_.each(buttonsList, function(button) {
FooterButtons.insert({text: button});
});
}
Run meteor and check your mongodb FooterButtons collection. Let me know. I'll make explanation if this work
If you need to update, then update it:
FooterButtons.update({text:'B'}, {$set:{text:'Extra'}});

Can't put data from a Meteor collection into an array

I'm learning Meteor and I was trying to pass the result of a Collection.find() into and array (using a variable) and the simpler code I have is (in a file that is in the root):
CalEvents = new Mongo.Collection('calevents'); //creating a collection
/*------------------------- Populating the database with dummy data-------*/
if (Meteor.isServer) {
Meteor.startup(function () {
if (CalEvents.find().count() === 0) {
CalEvents.insert({
title: "Initial room",
start: '2010-02-02'
});
}
});
}
/*--------------- Creating an array from the collection-----------------*/
events = [];
calEvents = CalEvents.find({});
calEvents.forEach(function(evt){
events.push({
title: evt.title,
start: evt.start,
})
});
The page has nothing to show but using the console I can see (CalEvents.find().fetch()) that I have data in my database but the "events" variable is empty...
I can't understand why because I tried several other things such as changing file names and moving code to guarantee the proper order.
And I already tried to use CalEvents.find().fetch() to create an array an put the result into a variable but I'm not able to do it...
Does anyone know what's so simple that I'm missing?...
Do you use autosubscribe?
You probably need to make sure the sbscription is ready. See Meteor: How can I tell when the database is ready? and Displaying loader while meteor collection loads.
The reason you do see CalEvents.find().fetch() returning items in the console is that by the time you make that call, the subscription is ready. But in your events = []; ... code (which I assume is in a file under the client directory, you might have assumed that the subscription data has arrived when in fact it has not.
A useful debugging tool is Chrome's device mode ("phone" icon near the search icon in DevTools), which lets you simulate slow networks (e.g. GPRS, with 500ms delay for every request).

Meteor minimongo dynamic cursor

In my client UI I have a form with differents search criterias, and I'd like to reactively update the results list. The search query is transformed into a classical minimongo selector, saved in a Session variable, and then I have observers to do things with the results:
// Think of a AirBnb-like application
// The session variable `search-query` is updated via a form
// example: Session.set('search-query', {price: {$lt: 100}});
Offers = new Meteor.Collection('offers');
Session.setDefault('search-query', {});
resultsCursor = Offers.find(Session.get('search-query'));
// I want to add and remove pins on a map
resultCursor.observe({
added: Map.addPin,
removed: Map.removePin
});
Deps.autorun(function() {
// I want to modify the cursor selector and keep the observers
// so that I would only have the diff between the old search and
// the new one
// This `modifySelector` method doesn't exist
resultsCursor.modifySelector(Session.get('search-query'));
});
How could I implement this modifySelector method on the cursor object?
Basically I think this method needs to update the compiled version of the cursor, ie the selector_f attribute, and then rerun observers (without losing the cache of the previous results). Or is there any better solution?
Edit: Some of you have misunderstood what I'm trying to do. Let me provide a complete example:
Offers = new Meteor.Collection('offers');
if (Meteor.isServer && Offers.find().count() === 0) {
for (var i = 1; i < 4; i++) {
// Inserting documents {price: 1}, {price: 2} and {price: 3}
Offers.insert({price:i})
}
}
if (Meteor.isClient) {
Session.setDefault('search-query', {price:1});
resultsCursor = Offers.find(Session.get('search-query'));
resultsCursor.observe({
added: function (doc) {
// First, this added observer is fired once with the document
// matching the default query {price: 1}
console.log('added:', doc);
}
});
setTimeout(function() {
console.log('new search query');
// Then one second later, I'd like to have my "added observer" fired
// twice with docs {price: 2} and {price: 3}.
Session.set('search-query', {});
}, 1000);
}
This doesn't solve the problem in the way you seem to be wanting to, but I think the result is still the same. If this is a solution you explicitly don't want, let me know and I can remove the answer. I just didn't want to put code in a comment.
Offers = new Meteor.Collection('offers');
Session.setDefault('search-query', {});
Template.map.pins = function() {
return Offers.find(Session.get('search-query'));
}
Template.map.placepins = function(pins) {
// use d3 or whatever to clear the map and then place all pins on the map
}
Assuming your template is something like this:
<template name="map">
{{placepins pins}}
</template>
One solution is to manually diff the old and the new cursors:
# Every time the query change, do a diff to add, move and remove pins on the screen
# Assuming that the pins order are always the same, this use a single loop of complexity
# o(n) rather than the naive loop in loop of complexity o(n^2)
Deps.autorun =>
old_pins = #pins
new_pins = []
position = 0
old_pin = undefined # This variable needs to be in the Deps.autorun scope
# This is a simple algo to implement a kind of "reactive cursor"
# Sorting is done on the server, it's important to keep the order
collection.find(Session.get('search-query'), sort: [['mark', 'desc']]).forEach (product) =>
if not old_pin?
old_pin = old_pins.shift()
while old_pin?.mark > product.mark
#removePin(old_pin)
old_pin = old_pins.shift()
if old_pin?._id == product._id
#movePin(old_pin, position++)
new_pins.push(old_pin)
old_pin = old_pins.shift()
else
newPin = #render(product, position++)
new_pins.push(newPin)
# Finish the job
if old_pin?
#removePin(old_pin)
for old_pin in old_pins
#removePin(old_pin)
#pins = new_pins
But it's a bit hacky and not so efficient. Moreover the diff logic is already implemented in minimongo so it's better to reuse it.
Perhaps an acceptable solution would be to keep track of old pins in a local collection? Something like this:
Session.setDefault('search-query', {});
var Offers = new Meteor.Collection('offers');
var OldOffers = new Meteor.Collection(null);
var addNewPin = function(offer) {
// Add a pin only if it's a new offer, and then mark it as an old offer
if (!OldOffers.findOne({_id: offer._id})) {
Map.addPin(offer);
OldOffers.insert(offer);
}
};
var removePinsExcept = function(ids) {
// Clean out the pins that no longer exist in the updated query,
// and remove them from the OldOffers collection
OldOffers.find({_id: {$nin: ids}}).forEach(function(offer) {
Map.removePin(offer);
OldOffers.remove({_id: offer._id});
});
};
Deps.autorun(function() {
var offers = Offers.find(Session.get('search-query'));
removePinsExcept(offers.map(function(offer) {
return offer._id;
}));
offers.observe({
added: addNewPin,
removed: Map.removePin
});
});
I'm not sure how much faster this is than your array answer, though I think it's much more readable. The thing you need to consider is whether diffing the results as the query changes is really much faster than removing all the pins and redrawing them each time. I would suspect that this might be a case of premature optimization. How often do you expect a user to change the search query, such that there will be a significant amount of overlap between the results of the old and new queries?
I have the same problem in my own hobby Meteor project.
There is filter session var where selector is storing. Triggering any checkbox or button changes filter and all UI rerender.
That solution have some cons and the main - you can't share app state with other users.
So i realized that better way is storing app state in URL.
May be it is also better in your case?
Clicking button now change URL and UI rendering based on it. I realize it with FlowRouter.
Helpful reading: Keeping App State on the URL

Resources