Backbone.js - printing out the collection and collection.models give different results - javascript

I have a fetch in my Backbone collection as follows.
var Items = Backbone.Collection.extend({
get_items:function(data) {
this.fetch({
data:data,
success:function() {
console.log(items);
}
});
});
}
var items = new Items();
items.get_items({id:1});
items.get_items({id:2});
In each of the console.log statement, I expect the contents of the collection to be different because I gave different parameters. But in Chrome, no matter what "id" value I give, the content of the collection doesn't change.
However, if I do
var Items = Backbone.Collection.extend({
get_items:function(data) {
this.fetch({
data:data,
success:function() {
console.log(items.models);
}
});
});
}
var items = new Items();
items.get_items({id:1});
items.get_items({id:2});
where I specifically printout "items.models", I can see that the list of models in the collection has indeed been changed.
What's going on here?

You're describing two different scenarios here: the collection is not just an array of models.. it has a lot of stuff attached to it. The collection's 'models' property is where all the data is. Only that is the absolute source of truth - anything else is either lying or doing something it's not supposed to.
Edit:
To add to this, why not reference 'this' instead of 'items'? i.e. this.models? After all, if you're wondering what is inside of the collection that you are working with, having to reference a global variable is a bit silly :) (and rather bad practice).
Edit #2:
Also, the 'fetch' is asynchronous unless specified otherwise.. so if you're trying to get a consistent, reproducible result, doing two fetches in a row isn't going to give it to you. If one of the requests is even a millisecond slower than the other for whatever reason then your results will be out of order anyway.

Related

Wait in angular until elements are loaded?

In angular we have an enrich method which runs some rest call to enrich a data object, then sets a variable which will cause a hidden details tab to be visible. Something like this overly simplified example:
$scope.enrich = function(team){
angular.forEach(team.members, function(member){
member.getSkills().then(function(skills){
member.skills=skills;
}
});
$scope.enrichFinished=true;
};
I'm getting exceptions in the detail pane which is opened when enrichFinished is true. The exceptions appear to be due to a filter which attempts to filter on member.skill and discovers skill is undefined for the member. I assume the problem is that we open the detail tab as soon as enrichFinished is set, which is before the then clause that sets member.skills=skill; thus we have a datarace where skills's may not yet be set by the time we try to filter on it.
What is the cleanest way to tell angular to wait to run the filter in the detail's tab until after I have actually generated and saved the data I need? The actual enrich method enriches 3-4 different variables within the for loop.
You can set $scope.enrichFinished=true; after all promisses solve, so you can use $q.all like this
$scope.enrich = function(team){
$q.all(team.members.map(
function(member){
return member.getSkills().then(
function(skills){
member.skills=skills;
}
);
}
)
).then(function(){
$scope.enrichFinished=true;
});
};

Appending new results to an open jQuery autocomplete menu

I have an app in which I have multiple search sources. Previously, the users had to choose in what source to search in before searching. If they did not choose, the app would default to one of the options.
However, now they want to search in all the sources at the same time. This is fine enough, but the problem is that when one of the searches returns, it overwrites the previous search result. Pretty much expected behavior. What I basically want is to append the new results to the already open autocomplete menu, instead of overwriting the old results. Naturally, the autocomplete menu would have to empty when it closes.
I guess that this is possible to do, but what approach is the best? I could just have an array I guess, which I append results to and then overwrite _renderMenu to use this array instead of the items one that is passed to the function. Then empty said array at the close event.
Is this the best way to go though? Or is there a more elegant solution?
Some code:
Ok, so searchAction is called by jquery autocomplete eventually. In collection.search I do the ajax call, here the URL is created based in the this parameter, then respondWhithData is called and maps the search result to a proper format (ie value and label for the autocomplete menu). After reponse is called from respondWithData, jquery automagically renders the resultsmenu. Thus, I probably have to overwrite the reponse event function as well as the _renderMenu and possibly _renderItem, yes?
searchAction: function(searchTerm, collection, response){
var self = this;
$.when(collection.search(searchTerm, this)).then(function(data) {
self.respondWithData(data, response);
});
},
respondWithData : function(data, response) {
if (data.length > 0) {
var responseVal = _.map(data, this.mapData);
this.checkResponseCount(responseVal);
response(responseVal);
}
else {
response(this.emptyResult());
}
},
To be clear, the problem is not the multiple search itself, but rendering the asynchronos results. I want to render the first results that come back, and then appends the rest as soon as they are returned from the server.
Edit 2:
Just tried to edit ui.content in the autocompleteresponse event, but any edit does not take once it renders for some reason...
Edit 3: Ah, ui.content can only be modified directly, not changed. If I push every single change instead of concating two arrays ui.content shows what I want.
It works I guess, but its not perfect.
I can figure how looks your scenario but I'm guessing:
You should have like:
function search1() {
$.ajax({ ...
success: function(data) {
$('#myResultsDiv").html(data)
}
});
}
etc
Instead of overwritting the #myResultsDiv you need to Append the results like:
function search1() {
$.ajax({ ...
success: function(data) {
$('#myResultsDiv").append(data)
}
});
}
Edit: You can also do something like this:
var resultsArray = [];
var searchDone = 0;
var totalSearchs = 5; //assuming 5 searches
function search1() {
function search1() {
$.ajax({ ...
success: function(data) {
//APPEND data to resultsArray
searchDone++;
if(searchDone==totalSearch) //syncronize the 5 searchs before render
renderSearchs(resultsArray);
}
});
}

How to update cached jquery object after adding elements via AJAX

I'm trying to write a plugin-like function in jQuery to add elements to a container with AJAX.
It looks like this:
$.fn.cacheload = function(index) {
var $this = $(this);
$.get("cache.php", {{ id: index }).done(function(data) {
// cache.php returns <div class='entry'>Content</div> ...
$(data).insertAfter($this.last());
});
}
and I would like to use it like this:
var entries = $("div.entry"),
id = 28;
entries.cacheload(id);
Think that this would load another "entry"-container and add it to the DOM.
This is works so far. But of course the variable that holds the cached jQuery object (entries) isn't updated. So if there were two divs in the beginning and you would add another with this function it would show in the DOM, but entries would still reference the original two divs only.
I know you can't use the return value of get because the AJAX-call is asynchronous. But is there any way to update the cached object so it contains the elements loaded via AJAX as well?
I know I could do it like this and re-query after inserting:
$.get("cache.php", {{ id: num }).done(function(data) {
$(data).insertAfter($this.last());
entries = $("div.entry");
});
but for this I would have to reference the variable holding the cached objects directly.
Is there any way around this so the function is self-contained?
I tried re-assigning $(this), but got an error. .add() doesn't update the cached object, it creates a new (temporary) object.
Thanks a lot!
// UPDATE:
John S gave a really good answer below. However, I ended up realizing that for me something else would actually work better.
Now the plugin function inserts a blank element (synchronously) and when the AJAX call is complete the attributes of that element are updated. That also ensures that elements are loaded in the correct order. For anyone stumbling over this, here is a JSFiddle: http://jsfiddle.net/JZsLt/2/
As you said yourself, the ajax call is asynchronous. Therefore, your plugin is asynchronous as as well. There's no way for your plugin to add the new elements to the jQuery object until the ajax call returns. Plus, as you discovered, you can't really add to the original jQuery object, you can only create a new jQuery object.
What you can do is have the plugin take a callback function as a second parameter. The callback could be passed a jQuery object that contains the original elements plus the newly inserted ones.
$.fn.cacheload = function(index, callback) {
var $this = this;
$.get('cache.php', { id: index }).done(function(html) {
var $elements = $(html);
$this.last().after($elements);
if (callback) {
callback.call($this, $this.add($elements));
}
});
return $this;
};
Then you could call:
entries.cacheload(id, function($newEntries) { doSomething($newEntries); } );
Of course, you could do this:
entries.cacheload(id, function($newEntries) { entries = $newEntries; } );
But entries will not be changed until the ajax call returns, so I don't see much value in it.
BTW: this inside a plugin refers to a jQuery object, so there's no need to call $(this).

Lawnchair.js not updating data

I've got a "LocalStore" object for storing data locally. It's based around a Lawnchair object.
var LocalStore = function(name) {
var that = this;
that.name = name;
that.lawnchair = Lawnchair({ name: that.name }, function(store) {
this.before('save', function(record){
console.log("saving " + that.name);
console.log(record);
});
this.after('save', function(record){
console.log("saved " + that.name);
console.log(record);
that.getData(function(records){
console.log("now it's this");
console.log(records);
});
});
});
that.getData = function(callback) {
that.lawnchair.get(that.name, callback);
};
};
LocalStore is then extended with _.extend(from the Underscore.js library) with this method:
save: function(collection, callback) {
this.lawnchair.save({ key:this.name, value: collection }, function(record) {
callback(record);
});
}
This code is used to update a Backbone.js Collection object to Lawnchair. The first time "save" runs for my Users Collection it saves correctly and shows that the object is a simple key/value pair where value is an Array.
Later in my code when a User selects a Default Project, I modify the Users Collection and call "save" again with an updated "defaultProjectId" on the User. The code runs error free, but the after('save') code for Lawnchair runs and shows me that:
- The record object returned is a key/value pair where value is a full Backbone.js Collection with the defaultProjectId property set correctly.
- The getData method that grabs the latest from the Database still shows as a key/value pair with value a simple Array and defaultProjectId is set incorrectly.
I'm at a loss as what to do. It should just be simply calling "lawnchair.save" updates the record, but it just doesn't do it.
Could you try this jsfiddle?
http://jsfiddle.net/QUgtg/1/
I have recreated your code. Instead of a backbone collection, I am passing in an array of objects. This seems to work. You can see the logging output in Firebug.
I have used my own extend code to add the save(). Though honestly, I don't see why you would want to do it that way, instead of just adding a property to the prototype. Your code may differ in that aspect.
If what I have posted works on your end, could you modify that code to show what are you doing differently? If possible, recreate the issue on jsfiddle...

Getting backbone.js to run a function after constructing a Collection?

I may be completely missing something here, but I have the following:
a Model which encapsulates 'all' the data (all JSON loaded from one URL)
the model has one (or more) Collections which it is instantiating with the data it got on construction
some code which I want to run on the Collection when the data is initialized and loaded
My question is about the composed Collection. I could do this outside the scope of the Collection, but I'd rather encapsulate it (otherwise what's the point of making it a 'class' with an initializer etc).
I thought I could put that code in the initialize() function, but that runs before the model has been populated, so I don't have access to the models that comprise the collection (this.models is empty).
Then I thought I could bind to an event, but no events are triggered after initialization. They would be if I loaded the Collection with a fetch from its own endpoint, but I'm not doing that, I'm initializing the collection from pre-existing data.
My question: How to get initialize code to run on the Collection immediately after it is initialized with data (i.e. this.models isn't empty).
Is it possible to do this without having to get 'external' code involved?
Okay here is the demo code, perhaps this will explain things better.
var Everything = Backbone.Model.extend({
url: "/static/data/mydata.json",
parse: function(data)
{
this.set("things", new Things(data.things, {controller: this}));
}
});
var Thing = Backbone.Model.extend({
});
var Things = Backbone.Collection.extend({
model: Thing,
initialize: function(data, options)
{
// HERE I want access to this.models.
// Unfortunately it has not yet been populated.
console.log("initialize");
console.log(this.models);
// result: []
// And this event never gets triggered either!
this.on("all", function(eventType)
{
console.log("Some kind of event happend!", eventType);
});
}
});
var everything = new Everything();
everything.fetch();
// Some manual poking to prove that the demo code above works:
// Run after everything has happened, to prove collection does get created with data
setTimeout(function(){console.log("outside data", everything.get("things").models);}, 1000);
// This has the expected result, prints a load of models.
// Prove that the event hander works.
setTimeout(function(){console.log("outside trigger", everything.get("things").trigger("change"));}, 1000);
// This triggers the event callback.
Unfortunately for you the collection gets set with data only after it was properly initialized first and models are reset using silent: true flag which means the event won't trigger.
If you really wanted to use it you can cheat it a bit by delaying execution of whatever you want to do to next browser event loop using setTimeout(..., 0) or the underscore defer method.
initialize: function(data, options) {
_.defer(_.bind(this.doSomething, this));
},
doSomething: function() {
// now the models are going to be available
}
Digging this an old question. I had a similar problem, and got some help to create this solution:
By extending the set function we can know when the collection's data has been converted to real models. (Set gets called from .add and .reset, which means it is called during the core function instantiating the Collection class AND from fetch, regardless of reset or set in the fetch options. A dive into the backbone annotated source and following the function flow helped here)
This way we can have control over when / how we get notified without hacking the execution flow.
var MyCollection = Backbone.Collection.extend({
url: "http://private-a2993-test958.apiary-mock.com/notes",
initialize: function () {
this.listenToOnce(this, 'set', this.onInitialized)
},
onInitialized:function(){
console.log("collection models have been initialized:",this.models )
},
set: function(models,options){
Backbone.Collection.prototype.set.call(this, models, options);
this.trigger("set");
}
})
//Works with Fetch!
var fetchCollection= new MyCollection()
fetchCollection.fetch();
//Works with initializing data
var colData = new MyCollection([
{id:5, name:'five'},
{id:6, name:'six'},
{id:7, name:'seven'},
{id:8, name:'eight'}
])
//doesn't trigger the initialized function
colData.add(new Backbone.Model({id:9,name:'nine'};
Note: If we dont use .listenToOnce, then we will also get onInitialized called every time a model is added to or changed in the collection as well.

Categories