I need to pass an id to a collection for use in the url (e.g. /user/1234/projects.json) but am not sure how to do this, an example would be wonderful.
The way my application is structured is on launch a collection of 'users' is pulled and rendered, I then want when a user is clicked their 'documents' are pulled from the server into a new collection and rendered in a new view. The issue is getting the user id into the documents collection to give the relevant URL for the documents.fetch().
think I've got it, here is an example:
//in the the view initialize function
this.collection = new Docs();
this.collection.project_id = this.options.project_id;
this.collection.fetch();
//in the collection
url: function() {
return '/project/api/' +this.project_id+'/docs';
}
Your user collection url should be set to /user. Once that's set, your models should utilize that url in order to do their magic. I believe (not completely positive) that if a model is in a collection, calling the 'url' method will return /user/:id. So all your typical REST-ish functionality will be utilized on '/user/:id'. If you are trying to do something with a relationship (a user has many documents) it's kind of rinse and repeat. So, for your documents collection (which belogs to user correct?) you'd set the url to 'user_instance.url/documents'.
To show a one to many relationship with a backbone model, you'd do something like this (upgrade to backbone 0.5.1 for urlRoot):
var User = Backbone.Model.extend({
initialize: function() {
// note, you are passing the function url. This is important if you are
// creating a new user that's not been sync'd to the server yet. If you
// did something like: {user_url: this.url()} it wouldn't contain the id
// yet... and any sync through docs would fail... even if you sync'd the
// user model!
this.docs = new Docs([], {user_url: this.url});
},
urlRoot: '/user'
});
var Doc = Backbone.Model.extend();
var Docs = Backbone.Collection.extend({
initialize: function(models, args) {
this.url = function() { args.user_url() + '/documents'; };
}
});
var user = new User([{id: 1234}]);
user.docs.fetch({ success: function() { alert('win') });
Why do you need to override the URL property of the collection with a function?.. you could do:
this.collection = new Docs();
this.collection.project_id = this.options.project_id;
this.collection.url = '/project/api/' + this.options.project_id + '/docs';
this.collection.fetch();
I like the answer from Craig Monson, but to get it working I needed to fix two things:
Binding the User url method before passing it to the Docs
A return statement from the url function in Docs
Updated example:
var User = Backbone.Model.extend({
initialize: function() {
// note, you are passing the function url. This is important if you are
// creating a new user that's not been sync'd to the server yet. If you
// did something like: {user_url: this.url()} it wouldn't contain the id
// yet... and any sync through docs would fail... even if you sync'd the
// user model!
this.docs = new Docs([], { user_url: this.url.bind(this) });
},
urlRoot: '/user'
});
var Doc = Backbone.Model.extend();
var Docs = Backbone.Collection.extend({
initialize: function(models, args) {
this.url = function() { return args.user_url() + '/documents'; };
}
});
var user = new User([{id: 1234}]);
user.docs.fetch({ success: function() { alert('win') });
Related
Having a Backbone model shared between several views is a rather common situation. Nevertheless, let's say this model is a UserModel. It handles several methods, allowing a user to register or login for instance.
And when the user is logged, fetch is called to get the user's data. Therefor the model can't fetch itself with this.fetch() in its initialize method.
Where shoud it be fetched from? How?
This is our simple UserModel:
const UserModel = Backbone.Model.extend({
// get url for 'me' (ie connected user)
url() {
return app.endpoint + '/users/me/' + app.conToken;
},
login(email, password, rememberMe, callback) {
…
},
signup(email, password, firstname, lastname, callback) {
…
}
});
Now let's say it's shared by both:
HomeView & CartView
app.HomeView = Backbone.View.extend({
template: app.tpl.home,
initialize() {
// model is passed # instanciation
this.model.fetch();
this.listenTo(this.model, 'sync', this.render);
},
render() {
…
}
});
app.CartView = Backbone.View.extend({
template: app.tpl.cart,
initialize() {
// model is passed # instanciation
this.model.fetch();
this.listenTo(this.model, 'sync', this.render);
},
render() {
…
}
});
Now if I instanciate HomeView, userModel will be fetched. But if at a later point, I instanciate CartView, this same model will be fetched again. And that makes a useless http request.
Basically, the model could fetch istself after a succesfull call to its login method, but user can arrive on a page or reload his browser being already logged in. Furthermore, a user can land on any page, there's no way to say he's going to HomeView prior to CartView.
There are two options I see. Either UserModel smartly handles multiple fetch calls like so:
const UserModel = Backbone.Model.extend({
// get url for 'me' (ie connected user)
url() {
return app.endpoint + '/users/me/' + app.conToken;
},
isSync() {
// an hour ago
let hourAgo = Date.now() - 3600000;
// data has been fetched less than an hour ago
if (this.fetchTime && hourAgo > this.fetchTime) return true;
return false;
},
fetch() {
// has not already fetched data or data is older than an hour
if (!this.isSync()) {
this.fetchTime = Date.now();
this.fetch();
return;
}
// trigger sync without issuing any http call
this.trigger('sync');
},
…
});
That way, I'm able to call this.model.fetch() as many times as needed, being stateless in views.
Or, I can handle that on a view layer:
app.HomeView = Backbone.View.extend({
template: app.tpl.home,
initialize() {
// model is passed # instanciation
// fetch model if empty
if (_.isEmpty(this.model.changed)) this.fetch();
// render directly if already populated
else this.render();
// render on model sync
this.listenTo(this.model, 'sync', this.render);
},
render() {
…
}
});
If needed, Backbone's model.changed doc reference & Underscore's _.isEmpty's.
Which way is cleaner? Is there any other approach I might have missed?
Personal preference would not be to override fetch but to instead implement a wrapper function, like customFetch
const UserModel = Backbone.Model.extend({
// get url for 'me' (ie connected user)
url() {
return app.endpoint + '/users/me/' + app.conToken;
},
isSync() {
// an hour ago
let hourAgo = Date.now() - 3600000;
// data has been fetched less than an hour ago
if (this.fetchTime && hourAgo > this.fetchTime) return true;
return false;
},
customFetch() {
// has not already fetched data or data is older than an hour
if (!this.isSync()) {
this.fetchTime = Date.now();
this.fetch();
return;
}
// trigger sync without issuing any http call
this.trigger('sync');
},
…
});
The code example you provided would end up in a loop (this.fetch calling itself...), so my personal preference is to just wrap the core backbone functionality in another function.
I would even go so far as to have my own custom Model that is extended by all the models I use. Eg:
const MyModel = Backbone.Model.extend({
isSync() {
// an hour ago
let hourAgo = Date.now() - 3600000;
// data has been fetched less than an hour ago
return (this.fetchTime && hourAgo > this.fetchTime);
},
customFetch() {
this.fetch();
},
});
Then UserModel would override customFetch and look like this:
const UserModel = MyModel.extend({
customFetch() {
// has not already fetched data or data is older than an hour
if (!this.isSync()) {
this.fetchTime = Date.now();
this.fetch();
return;
}
// trigger sync without issuing any http call
this.trigger('sync');
},
});
Might not be the best way to do it. For me personally it would be the easy way for it to read and then extend later. I would imagine this customFetch would be used in some/all models so it could be amended as appropriate.
i am facing problem in backbone collection.
this is my router
var LanguageRouter = Backbone.Router.extend({
routes: {
'': 'defaultaction',
'section/:key': 'sectionview',
}
});
collection is
var LanguageCollection = Backbone.Collection.extend({
model: LanguageModel,
url: '/lang'
});
and app.js
var initialize = function () {
var language_router = new LanguageRouter(),
parent_view = new ParentView(),
list_collection = new LanguageCollection(),
list_collection.fetch();
language_router.on('route:defaultaction', function () {
list_view = new LanguageListView({
collection: list_collection,
template: _.template(templates.languagelistsingle)
});
});
Here , after fetching the list_collection i tried passed the collection to language_view but i am getting empty collection only. How to fix this... Thanks in advance
Fetch is not synchronous, so you cannot call it in one line and use the return on the next line. The correct way using backbone is use listenTo or On (depending on the backbone version)
You could change this part:
list_collection = new LanguageCollection();
this.listenTo(list_collection, "sync", this.someFunction());
\\here we are listening the sync event that is automatically fired by backbone when a model or collection is synced with the server.
list_collection.fetch();
someFunction:function(){
//Logic with the collection content...
}
To learn more about events, please take a read here: http://backbonejs.org/#Events-catalog
I just recently started using Backbone.js and I'm working on an app now using Brunch that does a JSONP request to an external API to populate my collection and models. I'm following these previous posts (this and this) on doing JSONP requests with Backbone, but my collection still isn't getting the data for some reason.
My model (app/models/model.js):
module.exports = Backbone.Model.extend({
});
My collection (app/models/collection.js):
var Post = require('./model');
module.exports = Backbone.Collection.extend({
model: Post,
url: "http://somedata.com/api/posts/list/stuff",
sync: function(method, model, options) {
options.timeout = 10000;
options.dataType = "jsonp";
options.jsonp = "JSONPcallback";
return Backbone.sync(method, model, options);
},
parse: function(response) {
if (response) {
var parsed = [];
for(var i = 0; i < response.results.length; i++) {
parsed.push(response.results[i][0]);
}
return parsed;
}
}
});
Then, in the initialize method in app/application.js I'm calling it by:
var Category = require('models/collection');
this.cat = new Category();
this.cat.fetch();
Now, when I look at the parse function in console.log, I see the data being fetched, so the request is going through successfully. However, when my views are rendered and I do console.log(application.cat.models) in app/views/view.js, I get nothing -- why's this happening? Is there anything wrong with the code on my model/collection?
Also, the JSONP data has the following format, which is why looping through for response.results[i][0] and returning an array with all of it, that should do the trick, right?
{"results":[
{"0":{"id":xxx,"title":xxx,"link":xxx},
"description":xxx},
{"0":{"id":xxx,"title":xxx,"link":xxx},
"description":xxx},
{"0":{"id":xxx,"title":xxx,"link":xxx},
"description":xxx},...
]}
Would really appreciate any help...
I have 2 comments here :
I see that you have names both your model and collection as module.exports , a common practice is to make the model as singular (module.export) and make the collection for those models plural module.exports , just common practice , nothing "wrong" otherwise
You can have 2 callbacks in your code , when the collection is done fetching data(asynchronous event) also considering module.exports as your collection here ,
A. You could do this :
module.exports.fetch({
success : function(data){
console.log(JSON.stringiy(data));
//do remaining programming here
}
});
B. you could have a event listener for reset , from the documentation here , the collection fires a reset event when it completes the fetch , so could add an event listener on the collection like this :
module.exports.on('reset',function(data){
console.log(JSON.stringify(data));
//do remaining programming here
},this);
Currently I am fetching a collection that has over 1000 models which has a decent delay. How can I fetch 50 at a time? Also, is it possible to hit a "more" button to fetch another 50 that is not currently there?
Trying to advoid grabing the entire collection at once and have more of a "lazy loading" type of scheme.
Here is my current render method
render: function(){
var self = this
var collection = this.collection
collection.each(function(tenant){
var view = new TenantView({
model: tenant,
collection: collection
})
self.$el.append(view.render().el)
})
return this
}
You have to specify {add: true} and your pagination arguments in collection.fetch call. It will append to collection instead of reseting its contents.
collection.fetch({data: {page: 3}, add: true})
Then simply listen to collection's add event and append item to your view.
UPDATE: in the current version of backbone you need to call:
collection.fetch({data: {page: 3}, remove: false});
From the backbone.org website under Collection method fetch.
Backbone.sync = function(method, model) {
alert(method + ": " + model.url);
};
var Accounts = new Backbone.Collection;
Accounts.url = '/accounts';
Accounts.fetch();
You could set a limit in the query string of the url like /accountants?offset=0&limit=50.
Limit the query results from your database using these variables (offset, limit).
Modify the query string variables after fetching the requested models so when the user presses a button or scrolls down on your page the request for the next batch of models would be /accountants?offset=50&limit=50
I would do this on the view itself, rather than overwriting sync or fetch itself.
Something like:
// when extending your view
initialize: function(options) {
//...
this.collection.on('add', this.renderTenant, this);
},
events: {
// change the selector to match your "more" button
'click button.more': 'uiMore'
},
// Just tacking this on the view. You could make it an option, or whatever.
perPage: 50,
// this would produce a query with `offset` and `length`. Change it to
// however your request should paginate: page/perPage, just page, etc.
uiMore: function() {
var $more = this.$('.more');
var data = {};
data.offset = this.collection.length;
data.length = this.perPage;
$more.prop('disabled', true);
this.collection.fetch({data: data, add: true, success: function() {
$more.prop('disabled', false);
});
},
renderTenant: function(tenant) {
var view = new TenantView({
model: tenant,
collection: this.collection
})
this.$el.append(view.render().el);
},
render: function(){
this.collection.each(this.renderTenant.bind(this));
return this;
}
I'm trying to add a list of models to a collection to be stored locally. I don't fully understand backbone yet which is really the cause of this problem.
I basically pull in an RSS feed, assign each item in the feed to a Model and try place the list of Models into a collection so I can iterate over them later.
I am getting an error saying that I need to specify a Url for the collection.
It would be brilliant if someone could explain to me the correct process I need to follow to achieve my goal.
Currently I have:
var DetailIndividual = Backbone.Model.extend();
var DetailsIndividual = Backbone.Collection.extend({
model: DetailIndividual
});
var Search = Backbone.View.extend({
events: {
'click a.individualCast' : 'pullIndividual'
},
initialize: function() {
this.detailsIndividual = new DetailsIndividual();
_this = this;
this.detailsIndividual.bind('reset', function(collection) {
collection.each(function(item) {
//code to handle update
});
});
},
pullIndividual: function(e){
e.preventDefault();
//Logic to pull in RSS feed
for (var i = 0; i < result.feed.entries.length; i++) {
entry[i] = new DetailIndividual({ title: result.feed.entries[i].title, link: result.feed.entries[i].link, });
}
this.detailsIndividual.add(entry);
}
});
The error is reported out from here,because model must have url attribute:
http://backbonejs.org/docs/backbone.html#section-167
do you model have url attribute?
The reason why you're getting the error is because you're binding the 'reset' event. 'reset' is only fired on a collection.fetch or an explicit call to collection.reset, and in your case you're never fetching from the server with your collection - I'm assuming from your code you already have the feed in memory - so unless you're explicitly resetting, there's no need to listen for the reset.
In your code, you're not really extending Collection and Model, so it's actually not necessary to make extended objects - just use Backbone.Collection. You don't even need to create a Model extension because by default, when you add a JSON, a Backbone.Model is automatically created. It's only necessary to assign the collection.model if you're creating a truly custom model (with method overrides and additions).
Here's a way you could load your collection:
var search = Backbone.View.extend({
events: {
'click a.individualCast' : 'pullIndividual'
},
initialize: function() {
this.detailsIndividual = new Backbone.Collection();
},
pullIndividual: function(e) {
e.preventDefault();
//Logic to pull in RSS feed
for (var i = 0; i < result.feed.entries.length; i++) {
this.detailsIndividual.add({
title: result.feed.entries[i].title,
link: result.feed.entries[i].link
});
}
}
});
You didn't provide any code of how you wanted to parse collection (except in the 'reset'), but essentially you'd load the collection from the feed as shown.