Using a Backbone Collection without a data source (no url) - backbone.js

First time posting here... looking forward to see how it all works, but have really appreciated reading other's questions & answers.
I am using backbone for a small app and have found it helpful to use a collection to store some information that is only required during the current session. (I have a number of collections and all the others connect to my API to store/retrieve data).
I read here (in backbone.js can a Model be without any url?) that it is possible, and even good to use a collection without providing a url.
Now I would like to add a row of data to the collection... simple:
myCollection.create(data);
but of course that now throws an error:
Uncaught Error: A "url" property or function must be specified
Is there any way to use a Backbone collection, be able to add new rows of data (models) to it, but not sync to any sort of data source. Or can you suggest another solution.
I guess I could just use an object to hold the data, but I was enjoying the consistency and functionality.
I am using Backbone.Marionette if that has any impact.
Thanks in advance.

One thing you could do is override the Backbone.Model methods that communicate with the server, i.e. sync, fetch, and save... for example:
var App = {
Models: {},
Collections: {}
};
App.Models.NoUrlModel = Backbone.Model.extend({});
App.Models.NoUrlModel.prototype.sync = function() { return null; };
App.Models.NoUrlModel.prototype.fetch = function() { return null; };
App.Models.NoUrlModel.prototype.save = function() { return null; };
App.Collections.NoUrlModels = Backbone.Collection.extend({
model: App.Models.NoUrlModel,
initialize: function(){}
});
var noUrlModels = new App.Collections.NoUrlModels();
noUrlModels.create({'foo': 'bar'}); // no error
// noUrlModels.models[0].attributes == {'foo': 'bar'};
See Demo

Related

Understanding BackboneJS flow

I have been given a Project which is written entirely in Backbone.js, which I am supposed to change according to our specific needs. I have been studying Backbone.js for the past 2 weeks. I have changed the basic skeleton UI and a few of the features as needed. However I am trying to understand the flow of the code so that I can make further changes.
Specifically, I am trying to search some content on Youtube. I have a controller which uses a collection to specify the url and parse and return the response. The code is vast and I get lost where to look into after I get the response. I tried to look into views but it only has a div element set. Could someone help me to proceed. I wont be able to share the code here, but a general idea of where to look into might be useful.
Code Snippet
define([
'models/youtubeModelForSearch',
'coretv/config',
'libs/temp/pagedcollection',
'coretv/coretv'
],function( youtubeModelForSearch, Config, PagedCollection, CoreTV ) {
"use strict";
return PagedCollection.extend({
model: youtubeModelForSearch,
initialize: function() {
this.url = 'http://gdata.youtube.com/feeds/api/videos/?v=2&alt=json&max-results=20';
},
fetch: function(options) {
if (options === undefined) options = {};
if (options.data === undefined) options.data = {};
//options.data.cmdc = Config.getCMDCHost();
//CoreTV.applyAccessToken(options);
PagedCollection.prototype.fetch.call(this, options);
},
parse: function(response) {
var temp = response.feed
/*temp["total"] = 20;
temp["start"] = 0;
temp["count"] = 10; */
console.log(temp);
return temp.entry;
},
inputChangeFetch: function(query) {
this.resetAll();
if(query) {
this.options.data.q = query;
// this.options.data.region = Config.api.region;
//this.options.data.catalogueId = Config.api.catalogueId;
this.setPosition(0);
}
}
});
});
Let's assume your collection endpoint is correctly set and working. When you want to get the data from the server you can call .fetch() on you collection.
When you do this, it will trigger an request event. Your views or anybody else can listen to it to perform any action.
When the data arrives from the server, your parse function is called, it is set using set or reset, depending the options you passed along fetch(). This will trigger any event related to the set/reset (see the documentation). During set/reset, the data retrieved from your server will be parsed using parse (you can skip it, passing { parse: false }.
Right after that, if you passed any success callback to your fetch, it will be called with (collection, response, options) as parameters.
And, finally, it will trigger a sync event.
If your server does not respond, it will trigger an error event instead of all this.
Hope, I've helped.

Checking dynamic amount of deferreds of for example file uploads

Context
The situation as follows: Users can upload files in an application. They can do this at any time (and number of times).
I would like to show a spinner when any uploading is being done, and remove it when no uploading is happening at the moment.
My approach
The uploads are handles by an external file upload plugin (like blueimp) and on it's add method I grab the jqXHR object and add these to a backbone collection (which are images in my application, so I use this in combination with Marionette's collectionviews).
The following is part of a function called in an onRender callback of a Marionette Itemview:
// Get the file collection
var uploadFiles = SomeBackBoneCollection;
// Track how many deferreds are expected to finish
var expected = 0;
// When an image is added, get the jqXHR object
uploadFiles.bind('add', function(model) {
// Get jqXHR object and call function which tracks it
trackUploads(model.get('jqXHR'));
// Do something to show the spinner
console.log('start the spinner!');
// Track amount of active deferreds
expected++;
}, this);
// Track the uploads
function trackUploads(jqXHR) {
$.when(jqXHR).done(function(){
// A deferred has resolved, subtract it
expected--;
// If we have no more active requests, remove the spinner
if (expected === 0) {
console.log('disable the spinner!');
}
});
}
Discussion
This method works very well, although I'm wondering if there are any other (better) approaches.
What do you think about this method? Regarding this method, do you see any up- or downsides? Any other methods or suggestions anyone?
For example, it might be great to have some kind of array/object to which you can keep passing deferreds, and that a $.when is somehow monitoring this collection and resolves if at any moment everything is done. However, this should work such that you can keep passing deferred objects at any given time.
you can do this via events.
I am assuming each file is an instance of this model:
App.Models.File = Backbone.Model.extend({});
before the user upload the file, you are actually creating a new model, and save it.
uploadedFiles.create(new App.Models.File({...}));
so in your upload view...
//listen to collection events
initialize: function() {
//'request' is triggered when an ajax request is sent
this.listenTo(this.collection, 'request', this.renderSpinner);
//when the model is saved, sync will be triggered
this.listenTo(this.collection, 'sync', this.handleCollectionSync);
}
renderSpinner: function() {
//show the spinner if it is not already being shown.
}
ok, so, in 'handleCollectionSync' function, you want to decide if we wanna hide the spinner.
so how do we know if there're still models being uploaded? you check if there're new models in the collection (not saved models)
so in your collection, add a helper method:
App.Collections.Files = Backbone.Collection.extend({
//if there's a new model, return true
hasUnsavedModels: function() {
return this.filter(function(model) {
return model.isNew();
}).length > 0;
}
});
back to your view:
handleCollectionSync: function() {
//if there's no unsaved models
if(!this.collection.hasUnsavedModels()){
//removespinner
}
}
this should solve your problem assuming all the uploads are successful. you may want to complete this with error handling cases - it depends on what you wanna do with error case, but as long as you are not retrying it right away, you should remove it from the collection.
==========================================================================================
Edit
I'm thinking, if you allow the user to upload a file multiple times, you are not really creating new models, but updating existing ones, so the previous answer would not work. to work around this, I would track the status on the model itself.
App.Models.File = Backbone.Model.extend({
initialize: function() {
this.uploading = false; //default state
this.on('request', this.setUploading);
this.on('sync error', this.clearUploading);
}
});
then setUploading method should set uploading to true, clearUploading should change it to false;
and in your collection:
hasUnsavedModels: function() {
return this.filter(function(model) {
return model.uploading;
}).length > 0;
}
so in your view, when you create a new file
uploadNewFile: function(fileAttributes) {
var newFile = new App.Model.File(fileAttributes);
this.collection.add(newFile);
newFile.save();
}
I believe 'sync' and 'request' events are triggered on the collection too when you save models inside of it. so you can still listenTo request, sync, and error events on the collection, in the view.

how backbone.js model fetch method works

i am very confuse about using backbone.js model fetch method. See the following example
backbone router:
profile: function(id) {
var model = new Account({id:id});
console.log("<---------profile router-------->");
this.changeView(new ProfileView({model:model}));
model.fetch();
}
the first step, the model account will be instantiated, the account model looks like this.
define(['models/StatusCollection'], function(StatusCollection) {
var Account = Backbone.Model.extend({
urlRoot: '/accounts',
initialize: function() {
this.status = new StatusCollection();
this.status.url = '/accounts/' + this.id + '/status';
this.activity = new StatusCollection();
this.activity.url = '/accounts/' + this.id + '/activity';
}
});
return Account;
});
urlRoot property for what is it? After model object created, the profileview will be rendered with this this.changeView(new ProfileView({model:model}));, the changeview function looks like this.
changeView: function(view) {
if ( null != this.currentView ) {
this.currentView.undelegateEvents();
}
this.currentView = view;
this.currentView.render();
},
after render view, profile information will not display yet, but after model.fetch(); statement execute, data from model will be displayed, why? I really don't know how fetch works, i try to find out, but no chance.
I'm not entirely sure what your question is here, but I will do my best to explain what I can.
The concept behind the urlRoot is that would be the base URL and child elements would be fetched below it with the id added to that urlRoot.
For example, the following code:
var Account = Backbone.Model.extend({
urlRoot: '/accounts'
});
will set the base url. Then if you were to instantiate this and call fetch():
var anAccount = new Account({id: 'abcd1234'});
anAccount.fetch();
it would make the following request:
GET /accounts/abcd1234
In your case there, you are setting the urlRoot and then explicitly setting a url so the urlRoot you provided would be ignored.
I encourage you to look into the Backbone source (it's surprisingly succinct) to see how the url is derived: http://backbonejs.org/docs/backbone.html#section-65
To answer your other question, the reason your profile information will not display immediately is that fetch() goes out to the network, goes to your server, and has to wait for a reply before it can be displayed.
This is not instant.
It is done in a non-blocking fashion, meaning it will make the request, continue on doing what it's doing, and when the request comes back from the server, it fires an event which Backbone uses to make sure anything else that had to be done, now that you have the model's data, is done.
I've put some comments in your snippet to explain what's going on here:
profile: function(id) {
// You are instantiating a model, giving it the id passed to it as an argument
var model = new Account({id:id});
console.log("<---------profile router-------->");
// You are instantiating a new view with a fresh model, but its data has
// not yet been fetched so the view will not display properly
this.changeView(new ProfileView({model:model}));
// You are fetching the data here. It will be a little while while the request goes
// from your browser, over the network, hits the server, gets the response. After
// getting the response, this will fire a 'sync' event which your view can use to
// re-render now that your model has its data.
model.fetch();
}
So if you want to ensure your view is updated after the model has been fetched there are a few ways you can do that: (1) pass a success callback to model.fetch() (2) register a handler on your view watches for the 'sync' event, re-renders the view when it returns (3) put the code for instantiating your view in a success callback, that way the view won't be created until after the network request returns and your model has its data.

Backbone.js Collection fetch success return

I am trying to get return from Backbone.js Collection fetch using code similar to one below. How do I bypass rest ?
var myData = getCollection();
getCollection(){
this.collection = fetchCollection.fetch({
success: function(collection, data) {
collectionData = fetchCollection.toJSON();
return collectionData;
}
},this);
}
fetch calls the underlying sync methods, Backbone.sync. You can either override the sync method of individual the collections or models, or replace Backbone.sync with your own. One such alternative sync implementation for instance is the Backbone localStorage adaptor which stores to localStorage instead of using REST to talk to a server.

How to not make backbone.js use the save response as model attributes

So when I save a model on the backend, My api send back a response telling everything went fine and giving you some other pointers in json format
My problem is that backbone think I want to use that response as attributes of my model and automatically dump them in the model attributes..
I just saved it on the front-end and do not want to save the attributs again.
That is the what Backbone.Model.parse is for. By default, it is just a pass-through, so you don't need to call "super".
Let's say you only care about two properties that come back (id and foo) and you don't care about anything else:
var myModel = Backbone.Model.extend({
parse : function(resp, xhr) {
return {
id: resp.id,
foo: resp.foo
};
}
});
Note that I included "id" in my example. It is really important that creates (POST) return an id property. Without it, the Backbone model won't know how to update/delete in the future. Even if it has a different name for id (like "objectId"), you should still set the id in this function.
Indeed that's the default behaviour, and if you want to change it, you have to overwrite some Backbone functions.
Looking at how save is implemented, you have two options - either overwrite save for your model, or overwrite parse to make it aware of the data you are sending.
http://documentcloud.github.com/backbone/docs/backbone.html#section-41
Or, you could give up sending the 'pointers' in the response, because an empty response means that the model does not change after save.
I have the exact issue you are encountering. Backbone is a pretty young framework with the additional fact that javascript is really dynamic. So the saying that there are a thousand ways to solve a problem applies really well here.
I think a more suitable way to go about this is to employ something called Mixins. Here's what I did:
define([
'underscore',
'backbone',
'jquery'
], function (_, Backbone, $) {
return {
parse: function(response, xhr){
var data = response;
if(response.response && response.response.status != 0){
return {};
}
if(response.response && response.response.data)
{
data = _.first(response.response.data);
if(typeof data == 'undefined'){
data={};
}
}
if(_.isFunction(this.postParse)){
return this.postParse.call(this, data);
}
return data;
}
}
});
As you can see, I've overridden the Backbone.Model.parse prototype method and came up with my own that takes in a response. And parse the response according to my server's spec. In you case, you would implement whatever it takes to understand your server's response.
With the ground work out of the way, specifying a model is very easy:
var group = Backbone.Model.extend(
_.extend({}, ModelMixin, {
initialize:function () {
}
})
);
return group;
Voila! All the parse methods and checks that you need to write is encapsulated in that one ModelMixin "superclass".

Resources