I’m trying to keep a Backbone.js Collection up-to-date with what’s happening on the server.
My code is similar to the following:
var Comment = Backbone.Model.extend({});
var CommentCollection = Backbone.Collection.extend({
model: Comment
});
var CommentView = Backbone.View.extend({ /* ... */ });
var CommentListView = Backbone.View.extend({
initialize: function () {
_.bindAll(this, 'addOne', 'addAll');
this.collection.bind('add', this.addOne);
this.collection.bind('refresh', this.addAll);
},
addOne: function (item) {
var view = new CommentView({model: item});
$(this.el).append(view.render().el);
},
addAll: function () {
this.collection.each(this.addOne);
}
});
var comments = new CommentCollection;
setInterval(function () {
comments.fetch();
}, 5000);
What happens is that when the comments are fetched, refresh is called, the same comments to the bottom of the CommentListView—which is what I’d expect from the code above.
What I’d like to know is what’s the best way to “refresh” the view, without loosing any “local state”.
Or just use the far simpler addition to backbone's fetch method:
this.fetch({ update: true });
When the model data returns from the server, the collection will be (efficiently) reset, unless you pass {update: true}, in which case it will use update to (intelligently) merge the fetched models. - Backbone Documentation
:-)
What you want to do is refresh the collection every few seconds and append the new comments. My suggestion is to deal with that problem on your backend. Send over the last timestamp from your last comment and ask the server for the delta from this date only.
To do so, in your collection:
CommentCollection = Backbone.Collection.extend({
url: function(){
return "/comments?from_time=" + this.last().get("created_at");
},
comparator: function(comment){
return comment.get("created_at");
}
});
In your backend, query your database based on the from_time parameter.Your client code does not change to refresh the view.
If you do not want to change your backend code for any reason add this line in the addAll function:
addAll: function(){
$(this.el).empty();
this.collection.each(this.addOne);
}
Backbone.Collection.merge([options])
Building on #Jeb's response above, I've encapsulated this behavior into a Backbone extension that you can copy and paste into a .js file and include in your page (after including the Backbone library itself).
It provides a method called merge for Backbone.Collection objects. Rather than fully resetting the existing collection (as fetch does), it compares the server response to the existing collection and merges their differences.
It adds models that are in the response, but not in the existing collection.
It removes models that are in the existing collection, but not in the response.
Finally, it updates the attributes of models found in the existing collection AND in the response.
All expected events are triggered for adding, removing, and updating models.
The options hash takes success and error callbacks which will be passed (collection, response) as arguments, and it provides a third callback option called complete that is executed regardless of success or error (mostly helpful for polling scenarios).
It triggers events called "merge:success" and "merge:error".
Here is the extension:
// Backbone Collection Extensions
// ---------------
// Extend the Collection type with a "merge" method to update a collection
// of models without doing a full reset.
Backbone.Collection.prototype.merge = function(callbacks) {
// Make a new collection of the type of the parameter
// collection.
var me = this;
var newCollection = new me.constructor(me.models, me.options);
this.success = function() { };
this.error = function() { };
this.complete = function() { };
// Set up any callbacks that were provided
if(callbacks != undefined) {
if(callbacks.success != undefined) {
me.success = callbacks.success;
}
if(callbacks.error != undefined) {
me.error = callbacks.error;
}
if(callbacks.complete != undefined) {
me.complete = callbacks.complete;
}
}
// Assign it the model and url of collection.
newCollection.url = me.url;
newCollection.model = me.model;
// Call fetch on the new collection.
return newCollection.fetch({
success: function(model, response) {
// Calc the deltas between the new and original collections.
var modelIds = me.getIdsOfModels(me.models);
var newModelIds = me.getIdsOfModels(newCollection.models);
// If an activity is found in the new collection that isn't in
// the existing one, then add it to the existing collection.
_(newCollection.models).each(function(activity) {
if (_.indexOf(modelIds, activity.id) == -1) {
me.add(activity);
}
}, me);
// If an activity in the existing collection isn't found in the
// new one, remove it from the existing collection.
var modelsToBeRemoved = new Array();
_(me.models).each(function(activity) {
if (_.indexOf(newModelIds, activity.id) == -1) {
modelsToBeRemoved.push(activity);
}
}, me);
if(modelsToBeRemoved.length > 0) {
for(var i in modelsToBeRemoved) {
me.remove(modelsToBeRemoved[i]);
}
}
// If an activity in the existing collection is found in the
// new one, update the existing collection.
_(me.models).each(function(activity) {
if (_.indexOf(newModelIds, activity.id) != -1) {
activity.set(newCollection.get(activity.id));
}
}, me);
me.trigger("merge:success");
me.success(model, response);
me.complete();
},
error: function(model, response) {
me.trigger("merge:error");
me.error(model, response);
me.complete();
}
});
};
Backbone.Collection.prototype.getIdsOfModels = function(models) {
return _(models).map(function(model) { return model.id; });
};
Simple Usage Scenario:
var MyCollection = Backbone.Collection.extend({
...
});
var collection = new MyCollection();
collection.merge();
Error Handling Usage Scenario:
var MyCollection = Backbone.Collection.extend({
...
});
var collection = new MyCollection();
var jqXHR = collection.merge({
success: function(model, response) {
console.log("Merge succeeded...");
},
error: function(model, response) {
console.log("Merge failed...");
handleError(response);
},
complete: function() {
console.log("Merge attempt complete...");
}
});
function handleError(jqXHR) {
console.log(jqXHR.statusText);
// Direct the user to the login page if the session expires
if(jqXHR.statusText == 'Unauthorized') {
window.location.href = "/login";
}
};
Make a duplicate collection. Fetch() it. Compare the two to find the deltas. Apply them.
/*
* Update a collection using the changes from previous fetch,
* but without actually performing a fetch on the target
* collection.
*/
updateUsingDeltas: function(collection) {
// Make a new collection of the type of the parameter
// collection.
var newCollection = new collection.constructor();
// Assign it the model and url of collection.
newCollection.url = collection.url;
newCollection.model = collection.model;
// Call fetch on the new collection.
var that = this;
newCollection.fetch({
success: function() {
// Calc the deltas between the new and original collections.
var modelIds = that.getIdsOfModels(collection.models);
var newModelIds = that.getIdsOfModels(newCollection.models);
// If an activity is found in the new collection that isn't in
// the existing one, then add it to the existing collection.
_(newCollection.models).each(function(activity) {
if (modelIds.indexOf(activity.id) == -1) {
collection.add(activity);
}
}, that);
// If an activity in the existing colleciton isn't found in the
// new one, remove it from the existing collection.
_(collection.models).each(function(activity) {
if (newModelIds.indexOf(activity.id) == -1) {
collection.remove(activity);
}
}, that);
// TODO compare the models that are found in both collections,
// but have changed. Maybe just jsonify them and string or md5
// compare.
}
});
},
getIdsOfModels: function(models) {
return _(models).map(function(model) { return model.id; });
},
Related
Multiple entity records have to be deleted in one call instead of multiple callbacks so trying to use Xrm.WebApi.online.executeMultiple to delete records. but the code written below is not working. Any help will be appreciated.
for (var i=0; i<Checkbox.length; i++)
{
if(Checkbox[i].checked)
{
var id = Checkbox[i].value;// GUID of the record to be deleted
Checkbox[i].checked = false;
DeleteRequests[i]={};
DeleteRequests[i].getMetadata = function(){
return{
boundParameter: undefined,
operationType: 2,
operationName: "Delete",
parameterTypes: {
}
}
}
DeleteRequests[i].etn="cme_entity";
DeleteRequests[i].payload=id;
}
}
window.parent.Xrm.WebApi.online.executeMultiple(DeleteRequests).then(
function (results) {alert("Success");},
function (error) {alert("Failed");});
Getting weird error that this operation could not be processed. Please contact Microsoft.
The issue has to do with how you are constructing the delete request objects. You need to declare a function that sets up the getMetadata function and the required entityReference object.
I've tested the below solution and it works.
var Sdk = window.Sdk || {};
Sdk.DeleteRequest = function (entityReference) {
this.entityReference = entityReference;
this.getMetadata = function () {
return {
boundParameter: null,
parameterTypes: {},
operationType: 2,
operationName: "Delete",
};
};
};
for (var i = 0; i < Checkbox.length; i++) {
if (Checkbox[i].checked) {
var id = Checkbox[i].value;
Checkbox[i].checked = false;
DeleteRequests[i] = new Sdk.DeleteRequest({ entityType: "account", id: id });
}
}
window.parent.Xrm.WebApi.online.executeMultiple(DeleteRequests).then(
function (results) { alert("Success"); },
function (error) { alert("Failed"); });
Unfortunately CRUD operations with Xrm.WebApi.online.execute and Xrm.WebApi.online.executeMultiple are not very well documented. I've written a blog post with some code samples.
The important parts are the declaration of the Sdk.DeleteRequest function as a property on window and instantiating a request object using new Sdk.DeleteRequest(). I experimented a little and determined that just simply creating a request object like you were doing before, even with the right attributes does not work either.
Hope this helps! :)
I've used matb33:collection-hooks to insert a document after inserting into another, is it possible to update an existing document following an insert? I'm trying to do the following:
within a template Box, whose data context has an _id of boxId, call a method to insert a new document into Targets collection
get the _id of the new document and add it to an array of the document with _id of boxId.
Since this refers to the new document in the hook, I can't figure out how to get the boxId to update the right document.
Final code here per Pawel's answer:
Template.Box.events({
'click .add button': function(e) {
e.preventDefault();
var currentBoxId = this._id;
var target = {
...
};
Meteor.call('targetAdd', target, currentBoxId, function(){});
}
});
Meteor.methods({
targetAdd: function(targetAttributes, currentBoxId) {
check(this.userId, String);
check(currentBoxId, String);
check(targetAttributes, {
...
});
var target = _.extend(targetAttributes, {
userId: user._id,
submitted: new Date()
});
var targetId = Targets.insert(target);
Boxes.update(currentBoxId, {$addToSet: {targets:targetId}});
return {
_id: targetId
};
}
});
Collection hooks don't know and don't have dependency on where the document was inserted/updated (that is one of the points of collection hooks - it doesn't matter where the operation comes from, the hook should always behave the same way).
What is more, even your targetAdd method doesn't have the boxId already - you would have to pass it as one of the parameters.
So in this case, you should pass the boxId as a parameter to targetAdd method and modify the box document in the method.
Use the collection hooks only for cases when context of a collection operation is not important.
You can just pass boxId to method and then to new record, after it it will appear in hook:
Template.Box.events({
'click .add button': function(e) {
e.preventDefault();
var target = {
...
};
Meteor.call('targetAdd', target, this._id, function(){});
}
});
Meteor.methods({
targetAdd: function(targetAttributes, boxId) {
check(this.userId, String);
check(boxId, String);
check(targetAttributes, {
...
});
var target = _.extend(targetAttributes, {
submitted: new Date(),
boxId: boxId
});
var targetId = Targets.insert(target);
return {
_id: targetId
};
}
});
Targets.after.insert(function () {
var targetId = this._id;
var boxId = this.boxId;
Boxes.update({_id:boxId}, {$addToSet: {targets: targetId}}, function () {
});
});
I have the model 'note' and the collection 'to_paste'.
I want to fetch 'to_paste' after my model 'note' was fetched successfully because in 'to_paste' I want to use some parameters from 'note'.
So, what's the event I should use?
I did the next things but I'm sure it's wrong (I just fetch collection in model success).
note.fetch({
success: function (collection, note1) {
var noteView = new Memo.Views.FullNote( {model: note}, {flag: 1 } );
var to_paste = new Memo.Collections.NotebookList();
to_paste.fetch({
success: function (collection) {
var notebooks_to_paste = new Memo.Views.NotebookListToDelete( {collection: to_paste} );
var notebook_id = note.get("notebook_id");
$('.example').html(notebooks_to_paste.render().el);
}
});
$('.content').html(noteView.render().el);
}
});
I'm making a quiz. I'm loading in the questions from a json file. The models have a 'use: ' attribute. I'm filtering the collection by use and plucking the IDs, then I'm getting a random ID for the question and setting use to false once it has been answered. In this way, a user will go through all the questions in a random order with no repeats. I'd like to keep track of the used questions for each user and when they have done them all reset the question bank.
I'm using backbone.dualstorage and it worked once and then I tried to reset the collection by going through each model and destroying it. Now I can't seem to repopulate the local collection with models because the remote models have the same IDs as the destroyed model IDs.
How can I re-add all the models in the remote collection to localStorage again?
//Question collection
var PlayCollection = Backbone.Collection.extend({
model: PlayModel,
url: "https://raw.githubusercontent.com/robertocarroll/barjeel-app/master/app/data/questions.json",
//when I set remote it works without local at all
//remote: function () {return true;}
});
//define new question collection
var newCollection = new PlayCollection();
// load data
newCollection.fetch({
success: function (newCollection, response, options) {
console.log("fetch questions success");
//this shows all the question IDs which I destroyed
console.log(newCollection.destroyedModelIds());
//this is empty
console.log(newCollection.dirtyModels());
}
});
function getQuestion() {
var theQuestions = newCollection.dirtyModels()[0].collection;
//get the IDs of all questions which haven't been used
var questions = theQuestions.chain()
.filter(function (m) {
return m.get('use')
})
.pluck('id')
.value();
console.log(questions);
if (questions.length > 0) {
// get random ID from question ID array
var rand = questions[_.random(questions.length - 1)];
console.log('chosen ID value: ' + rand);
//get a model from a collection, specified by ID
var currentQuestion = theQuestions.get(rand);
console.log(currentQuestion);
//set the status of that question to used
currentQuestion.set("use", false);
}
//if there's not more questions
else {
console.log("No more questions");
//delete models in local
_.chain(newCollection.models).clone().each(function (model) {
console.log('deleting model ' + model.id);
model.destroy();
});
}
}
Here's the fiddle: http://jsfiddle.net/robertocarroll/10xqvk18/10/
Thanks!
Turns out backbone.dualstorage was overkill. I accessed localStorage directly instead:
//model for questions
PlayModel = Backbone.Model.extend({});
//model for which questions to use
PlayGameQuestionCount = Backbone.Model.extend({
defaults: {
"use": true
}
});
//Question collection
var PlayCollection = Backbone.Collection.extend({
model: PlayModel,
url: "https://raw.githubusercontent.com/robertocarroll/barjeel-app/master/app/data/questions.json"
});
//define new question collection
var newCollection = new PlayCollection();
function fetchQuestions() {
// load data
newCollection.fetch({
success: function (newCollection, response, options) {
console.log("fetch questions success");
//add data to local storage
localStorage.setItem('questions', JSON.stringify(newCollection.toJSON()));
}
});
}
function getQuestion() {
var newLocalCollection = new PlayCollection(JSON.parse(localStorage.getItem('questions')));
console.log(newLocalCollection);
//get the IDs of all questions which haven't been used
var questions = newLocalCollection.chain()
.filter(function (m) {
return m.get('use')
})
.pluck('id')
.value();
console.log(questions);
if (questions.length > 0) {
// get random ID from question ID array
var rand = questions[_.random(questions.length - 1)];
console.log('chosen ID value: ' + rand);
//get a model from a collection, specified by ID
var currentQuestion = newLocalCollection.get(rand);
console.log(currentQuestion);
//set the status of that question to used
currentQuestion.set("use", false);
localStorage.setItem('questions', JSON.stringify(newLocalCollection.toJSON()));
}
//if there's not more questions
else {
console.log("No more questions");
//delete models in local
fetchQuestions();
}
}
//function to fire the questions
$(document).ready(function () {
$("#btnSave").click(
function () {
getQuestion();
});
});
Here's the fiddle: http://jsfiddle.net/hcqse7j4/3/
I'm using meteor and I have a question about the publish function (server side)
Meteor.publish('users', function () { .... }
I'm sending now documents to the browser which have id's of other collections. For example the Task document belongs to a project
{
title: '....',
projectId: 'KjbJHvJCHTCJGVY234',
...
}
What I want is to add a property to the this document projectTitle so I don't have to look up the project on the client. However, when I add this property in the publish function it is not send to the client. This is what I've tried:
Meteor.publish('tasks', function () {
var tasks = Tasks.find();
tasks.forEach(function (task) {
var project = Projects.findOne({_id: task.projectId});
task.projectTitle = project.title;
});
return tasks;
}
Any suggestions how to modify documents (not persistent) inside the publish function?
You could do this:
Meteor.publish("tasks", function() {
var transform = function(task) {
var project = Projects.findOne({_id: task.projectId});
task.projectTitle = project.title;
return task;
}
var self = this;
var tasks = Tasks.find().observe({
added: function (document) {
self.added('tasks', document._id, transform(document));
},
changed: function (newDocument, oldDocument) {
self.changed('tasks', document._id, transform(newDocument));
},
removed: function (oldDocument) {
self.removed('tasks', oldDocument._id);
}
});
self.ready();
self.onStop(function () {
tasks.stop();
});
});
There's a lot of custom logic there, but the 'transform' basically adds the attributes in.
Your code looks good but you're forgetting the .fetch() method on your task request. It should be var tasks = Tasks.find().fetch();