backbone.js - getting extra data along with the request - javascript

I have a collection which holds some of the users. Some information that is needed is how many total there are, how many pages, etc. How do I pass these back to the client? Or do they have to come from a separate view in which case I will need more than one ajax call? I'd like to have the collection fetch() and also receive some of this "meta data". What's a good way for handling this?

Generally, you need to handle this in the collection class' parse method. Its responsibility is to take the response and return back an array of model attributes. However, you could do more than that if you wished if you didn't mind the parse method having this additional responsibility.
UserList = Backbone.Collection.extend({
model: User,
url: '/users',
parse: function(data) {
if (!data) {
this.registered_users = 0;
return [];
}
this.registered_users = data.registered_users;
var users = _(data.users).map(
function(user_data) {
var user = {};
user['name'] = user_data.name;
return user;
}
);
return users;
    }
});
So in the trivial example above, presume the server returns a response which contains a count of registered users and and an array of user attributes. You would both parse through and return the user attributes and you would pick off the registered user count and just set it as a variable on the model.
The parse method would get called as part of a fetch. So no need to modify the fetch, just use the built-in hook method that you have.
Purists would say that you are giving the parse method a secondary responsibility which might surprise some people (e.g. returning something and modifying model state). However, I think this is okay.

One way to do this is to override the Collection::fetch() method so that it parses this metadata out of the response. You could have your backend return a response like this:
{
"collection": [
{ ... model 1 ... },
{ ... model 2 ... },
...
],
"total_rows": 98765,
"pages": 43
}
In your fetch method which overrides the original Backbone.Collection::fetch() method, you can handle each property of the object separately. Here's you could do the override with a slightly modified fetch method:
_.extend(Backbone.Collection.prototype, {
fetch : function(options) {
options || (options = {});
var collection = this;
var success = options.success;
options.success = function(resp) {
// Capture metadata
if (resp.total_rows) collection.total_rows = resp.total_rows;
if (resp.pages) collection.pages = resp.pages;
// Capture actual model data
collection[options.add ? 'add' : 'refresh'](
collection.parse(resp.collection), options);
// Call success callback if necessary
if (success) success(collection, resp);
};
options.error = wrapError(options.error, collection, options);
(this.sync || Backbone.sync).call(this, 'read', this, options);
return this;
});
Note that this approach using _.extend will affect all your classes which extend Backbone.Collection.
This way, you don't have to make 2 separate calls to the backend.

I would bootstrap the information at pagecreation. Write the information into the html document when the server creates the site. Like that you don't have to have an ajax call at all. I do that with the whole collection in ordner not to first load the page and then wait for the ajax call to return the information needed.
Code example with Python:
Line 64: https://github.com/ichbinadrian/maps/blob/master/python/main.py <- from here
Line 43: https://github.com/ichbinadrian/maps/blob/master/templates/index.html <- into here

Related

backbone collection url does not take parameters

I have a Backbone Collection class with the url property set to a method.
The url is comprised of 3 "elements" which I have called baseUrl, username and path - so it would look something like baseUrl/username/path
I use the same instance for several users in the spa I am working on. However the one element I want to be able to change (after instantiation) is the username. I need this for 2 reasons.
1st - I don't have the username property at instantiation time.
2nd - The username changes depending on the user and I don't want to create new instances of the collection for every new user.
So my issue is why does the url method not take parameters after instantiating the collection so that I could do something like myCollection.url({username: "Mac"})?
Also why does Backbone not allow for parameters to be passed into the url method?
Backbone calls the url method with the collection as the context, so you can access member variables of your collection instance with this.
var MyCollection = Backbone.Collection.extend({
url: function() {
return "base-url/" + this.username + "/path";
},
});
To use this after instanciation, you could set the username field of your collection manually or you could override the sync method of your collection.
var MyCollection = Backbone.Collection.extend({
url: function() {
return "base-url/" + this.username + "/path";
},
sync: function(method, model, options) {
options = options || {};
if (options.username) this.username = options.username;
// call the default sync
return Backbone.collection.prototype.sync.call(this, method, model, options);
}
});
And use it:
myCollection.fetch({ username: "Mac" });
Additional information on how to know how url is called
I'm an avid user of the annotated source of Backbone and it's really important as a lot of Backbone's features are undocumented and the sources are small and easy to go through.
If you check the sync method, you should see that line:
params.url = _.result(model, 'url') || urlError();
And if you check the underscore's result function, you can see that it calls the function (passed as a string) like this:
return _.isFunction(value) ? value.call(object) : value;

Backbone collection fetch imported incorrectly

I have a collection which is fetched from a REST endpoint, where it receives a JSON.
So to be completely clear:
var Products = Backbone.Collection.extend({
model: Product,
url : 'restendpoint',
customFilter: function(f){
var results = this.where(f);
return new TestCollection(results);
}
});
var products = new Products();
products.fetch();
If I log this, then I have the data. However, the length of the object (initial) is 0, but it has 6 models. I think this difference has something to do with what is wrong, without me knowing what is actually wrong.
Now, if I try to filter this:
products.customFilter({title: "MyTitle"});
That returns 0, even though I know there is one of that specific title.
Now the funky part. If I take the ENTIRE JSON and copy it, as in literally copy/paste it into the code like this:
var TestCollection = Backbone.Collection.extend({
customFilter: function(f){
var results = this.where(f);
return new TestCollection(results);
}
});
var testCollectionInstance = new TestCollection(COPY PASTED HUGE JSON DATA);
testCollectionInstance.customFilter({title: "MyTitle"});
Now that returns the 1 model which I was expecting. The difference when I log the two collections can be seen below. Is there some funky behaviour in the .fetch() I am unaware of?
Edit 2: It may also be of value that using the .fetch() I have no problems actually using the models in a view. It's only the filtering part which is funky.
Edit 3: Added the view. It may very well be that I just don't get the flow yet. Basically I had it all working when I only had to fetch() the data and send it to the view, however, the fetch was hardcoded into the render function, so this.fetch({success: send to template}); This may be wrong.
What I want to do is be able to filter the collection and send ANY collection to the render method and then render the template with that collection.
var ProductList = Backbone.View.extend({
el: '#page',
render: function(){
var that = this; /* save the reference to this for use in anonymous functions */
var template = _.template($('#product-list-template').html());
that.$el.html(template({ products: products.models }));
//before the fetch() call was here and then I rendered the template, however, I needed to get it out so I can update my collection and re-render with a new one (so it's not hard-coded to fetch so to speak)
},
events: {
'keyup #search' : 'search'
},
search : function (ev){
var letters = $("#search").val();
}
});
Edit: New image added to clearify the problem
It's a bit tricky, you need to understand how the console works.
Logging objects or arrays is not like logging primitive values like strings or numbers.
When you log an object to the console, you are logging the reference to that object in memory.
In the first log that object has no models but once the models are fetched the object gets updated (not what you have previously logged!) and now that same object has 6 models. It's the same object but the console prints the current value/properties.
To answer your question, IO is asynchronous. You need to wait for that objects to be fetched from the server. That's what events are for. fetch triggers a sync event. Model emits the sync when the fetch is completed.
So:
var Products = Backbone.Collection.extend({
model: Product,
url : 'restendpoint',
customFilter: function(f){
var results = this.where(f);
return new TestCollection(results);
}
});
var products = new Products();
products.fetch();
console.log(products.length); // 0
products.on('sync',function(){
console.log(products.length); // 6 or whatever
products.customFilter({title: 'MyTitle'});
})
It seems like a response to your ajax request hasn't been received yet by the time you run customFilter. You should be able to use the following to ensure that the request has finished.
var that = this;
this.fetch({
success: function () {
newCollection = that.customFilter({ title: 'foo' });
}
});

how to pass an id containing / in backbone.js

HI my basic model which fetches data from server is working perfect. I want to implement a search feature. When user enters any data the request goes to browser and desired model is returned.
var Book = Backbone.Model.extend({
urlRoot: '/books'
});
render: function(options) {
books = new Book({id:options.name});
books.fetch();
}
where
name = "search/"+dynamic_data;
Request URL that is being formed when i pass --> 'life' in variable dynamic_data
http://host/path/search%2Flife
Request URL that i want
http://host/path/search/life
How can I encode/escape my string to achieve the desired result. I have tried escape(), encodeURI(), encodeURIComponents
A workaround to solve this is create one more model with urlRoot as /books/search and pass just name . I don't think this is correct. Should I use this ?
According to your additionnal precisions stating that life is actually a book name...
It looks like Backbone is better integrated with RESTful API's. In REST, your urls should not contain verbs and to search books, you would do a GET /books/?name=life.
In that case, you would only have to define a Backbone.Collection like:
var BooksCollection = Backbone.Collection.extend({
model: Book,
url: '/books'
});
The to fetch books:
var books = new BooksCollection();
books.fetch({data : {name: 'life'}}); //GET /books/?name=life
If you really want your search operation to target /search/:name, you will have to check the Backbone.Collection api, but I think you will want to look at http://backbonejs.org/#Sync
You could override your collection's sync method to something like:
Backbone.Collection.extend({
...
sync: function (method, model, options) {
//for read operations, call the search url
if (method === 'read') {
options.url = '/search/' + options.data.name;
delete options.data.name;
}
//call the default sync implementation
return Backbone.Collection.prototype.sync.apply(this, arguments);
}
});
In this cased calling books.fetch({data : {name: 'life'}}); will result in GET /books/life.
Here's a fiddle that shows the example.
this would work:
books = new Book({id:options.name}, {url: options.name));
decodeURIComponent() will decode http://host/path/search%2Flife to http://host/path/search/life.

backbone.js change url parameter of a Model and fetch does not update data fetched

I have the following Model:
window.MyModel = Backbone.Model.extend({
initialize: function(props){
this.url = props.url;
}
parse: function(){
// #override- parsing data fetched from URL
}
});
// instantiate
var mod = new MyModel({url: 'some/url/here'});
I use this global variable 'mod' to fetch some data into this model from backend.
// fetch
mod.fetch({
success: function(){ ...},
error: ...
});
All above works well....
My Issue: I want to reuse this model by changing resetting the url and call fetch but it does not update the url somehow. I have tried the following:
mod.fetch({
data: {url:'/some/other/url'},
postData: true,
success: function(){ //process data},
error: ...
});
mod.set({url: '/some/other/url'});
// called fetch() without data: and postData: attributes as mentioned in previous
How do I set the url for my model so that I could call fetch() and it fetches data from updated url? Am I missing something. Thanks for any pointers..
UPDATE 1: Basically, I am unable to get updated values if I did
model.set({url: 'new value'});
followed by
model.fetch();
'model' is a global variable. Creating a fresh instance of 'model' works:
model = new Model({url:'/some/other/url'});
model.fetch();
however, works as required. Does this mean that a model instance is permanently attached to a url and it cannot be reset?
ANSWER TO MY QUESTION in UPDATE 1 Model instance is not permanently attached to a url. It can be reset dynamically. Please read through #tkone's thorough explanation and then #fguillens' solution for a better understanding.
After have understood the #tkone 's explanation...
If you still want to have a dynamic Model.url you always can delay its construction to run time, try this:
window.MyModel = Backbone.Model.extend({
url: function(){
return this.instanceUrl;
},
initialize: function(props){
this.instanceUrl = props.url;
}
}
Well the answer here is that you want to do:
mod.url = '/some/other/url'
The URL isn't part of the instance of the model itself, but rather an attribute of the MyModel object that you're creating your model instance from. Therefore, you'd just set it like it was an normal JavaScript object property. set is used only when the data you're setting (or conversely getting with get) is actually an attribute of the data you want to send/receive from the server.
But why you're changing the URL is the question we should be asking. The idea behind Backbone's model/collection system is that you speak to a REST endpoint and each model has a corresponding endpoint.
Like you've got a blog and that blog has an "entry" object which is available at:
/rest/entry/
And you've got a Backbone model for Entry:
Entry = Backbone.Model.extend({urlBase: '/rest/entry'});
Now when you save or fetch Backbone knows how this works.
So like you're making a new model:
e = new Entry();
e.set({title: "my blog rulez", body: "this is the best blog evar!!!!1!!"});
e.save();
This would then make Backbone do an HTTP POST request to /rest/entry with the body:
{
"title": "my blog rulez",
"body": "this is the best blog evar!!!!1!!"
}
(When you do your mod.set({url: '/some/other/url'}); you're actually adding a field called url to the dataset, so the server would send "url": "/some/other/url" as part of that JSON POST body above:
{
"title": "my blog rulez",
"body": "this is the best blog evar!!!!1!!",
"url": "/some/other/url"
}
The server would then respond with an HTTP 200 (or 201) response with the same model, only with, like, say, and ID attached:
{
"id": 1,
"title": "my blog rulez",
"body": "this is the best blog evar!!!!1!!"
}
And that's not what you're looking for, right?)
Now you've got this model and it's got an ID. This means if you change it:
e.set('title', 'my blog is actually just ok');
e.save()
Backbone now makes an HTTP PUT request on /rest/entry/1 to update the resource on the server.
The server sees that you're talking about ID 1 on the /rest/entry/ endpoint, so knows to update an existing record (and send back an HTTP 200).
TL;DR
Don't change the URL, Backbone will. Make a new model for a new piece of data.
model.urlRoot = "/your/url"
OR
model.urlRoot = function(){ return "/your/url"; }
OR
model.url = "/your/url"
OR
model.url = function(){ return "/your/url"; }
Default 'url' property of a Backbone.Model object is as below. Backbone.js doc says:
Default URL for the model's representation on the server -- if you're using Backbone's restful methods, override this to change the endpoint that will be called.
url: function() {
var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();
if (this.isNew()) return base;
return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id);
},
Clearly, It first gets the value of urlRoot, if not available, it will look at the url of the collection to which model belongs. By defining urlRoot instead of url has an advantage of falling back to collection url in case urlRoot is null.
You can set url as option in fetch function, like this:
var mod = new MyModel();
mod.fetch({
url: '/some/other/url',
data: {}
});

How to get the id when I create a save a new model

In my Marionette.CompositeView it will be possible to create a new model making a put request to the server(1).
The put request is ok, but when I add the new model to the collection, the new model misses the id which is created by the server.
How should I fix this issue?
Should
1) the POST request send the id to the client or
2) I have to make another request from the client to get the id?
(1)
return Marionette.CompositeView.extend({
submitForm: function (event) {
this.textAreaElement = this.$el.find('[data-tid="announcement"]');
this.messageModel = new MessageModel();
this.messageModel.save({
message: this.textAreaElement.val()
}, {
wait: true,
success: this.onSuccess,
error: this.onError
});
},
onSuccess: function () {
console.log(this.messageModel.get('id')); // undefined
this.collection.add(this.messageModel); // I need to get also the id of the following model
// which is created by the server
}
});
Your server needs to respond to the POST with JSON representing the saved model, including the ID (or just the the ID). For example, return {"id": "123"} and backbone will update the model for you.
In the options you pass to save() add wait:true. The attributes of the model (including id) will then be set to whatever json object the server returns for this request.
Backbone->Create and of course, like #Robert said, you can add wait:true so that your view is not updated until the server sends back your model (including the ID)

Categories

Resources