I'm trying to specify a function url for a backbone model like this:
var Directory = Backbone.Model.extend({
defaults: {
path: '/',
entries: new DirectoryEntries
},
initialize: function() {
this.listenTo(this, 'change:path', this.fetch);
this.fetch();
},
parse: function(response, options) {
if (response != null) {
for (var i = 0; i < response.length; ++i) {
this.get('entries').add(new DirectoryEntry(response[i]));
}
}
this.trigger('change');
},
url: function() {
return '/svn-ls.php?path=' + this.get('path');
},
The initial call to fetch() in my initialize function seems to work fine, but when fetch gets called on a change to path, backbone tries to make the following JSON request: http://localhost:8000/function%20()%20%7B%20%20%20%20%20%20return%20'/svn-ls.php?path='%20+%20this.get('path');%20%20%20%20}
Ie, it seems to be trying to use this.url instead of actually calling it.
Anyone know what the problem is here?
edit: I changed the listenTo call to the following:
var self = this;
this.listenTo(this, 'change:path', function() { self.fetch(); });
and now everything seems to work. I have no idea why binding this.fetch directly messed things up, though.
Related
I'm trying to use alter the xhr object on an ajax request. I'm doing this on a fetch call for a collection. But when I alter the xhr I get no data. The purpose of this is to show the loaders percentage but the xhr isn't even working when I return the new xhr object. I did checkout the xhr that is returned and the url points to /admin/categories
require(['views/categories', 'models/categories', 'helpers/helper'], function(CategoriesView, model, helper) {
var categories = new model.CategoriesCollection;
categories.fetch({ url: "/admin/categories/getcategories", xhr: helper.xhr('#main-loader') }).then(function(response) {
console.log(response);
});
});
and here is my helper file
define(['helpers/helper', 'require'], function(Helper, require) {
'use strict';
var $ = require('jquery');
var Backbone = require('backbone');
var xhr = function(loaderId) {
var _xhr = Backbone.$.ajaxSettings.xhr();
_xhr.addEventListener("progress", function(e){
if (e.lengthComputable) {
console.log(e);
}
}, false);
return _xhr;
}
return {
xhr: xhr
}
});
An easy way to pass options to the xhr object (XMLHttpRequest object) is to use the xhrFields option of the jQuery ajax function. Since Backbone.sync uses jQuery.ajax by default in the background, any options passed to a Backbone syncing function is then passed as the ajax options.
Simple one-off solution
The simplest example of checking progress:
myCollection.fetch({
url: root + '/photos/',
xhrFields: {
onprogress: function() {
console.log("options onprogress");
}
}
});
Permanent solution
But a more convinient way would be to override the global Backbone.sync function to add our own progress callback option and a custom progress event.
Overriding Backbone.sync
Warning: don't override Backbone core if you're writing a library or code that will be shared.
Backbone.sync = (function(syncFn) {
return function(method, model, options) {
options = options || {};
var context = options.context,
progress = options.progress,
xhrFields = options.xhrFields || {},
onprogress = xhrFields.onprogress;
xhrFields.onprogress = function(e) {
var params = [model, e.loaded, _.extend({}, options, { event: e })];
if (progress) progress.apply(context, params)
if (onprogress) onprogress.apply(this, arguments);
model.trigger(['progress'].concat(params));
};
options.xhrFields = xhrFields;
return syncFn.apply(this, arguments);
};
})(Backbone.sync);
How to use
It's really straight-forward to use:
var myCollection = new Backbone.Collection(),
root = 'https://jsonplaceholder.typicode.com';
It provides a custom progress event.
myCollection.listenTo(myCollection, 'progess', function(collection, value, options) {
console.log("collection progress event");
});
It also provides a custom progress callback that can be passed to any Backbone functions that calls Backbone.sync in the background, like fetch, save, destroy. Also, passing xhrFields still works as expected.
myCollection.fetch({
url: root + '/photos/',
success: function() {
console.log(myCollection.models);
},
// custom options callback
progress: function(collection, value, options) {
console.log("collection onprogress callback");
},
// this still works
xhrFields: {
onprogress: function() {
console.log("options onprogress");
}
}
});
You receive the collection or model, the loaded data count, and the options object, which contains all the options of the sync, in addition to the native progress event (options.event).
Note that it's not that useful as the total doesn't always work. As an example, in Chrome the total is always zero, but in firefox, the total is correct. You should check the lengthComputable property.
In a model I have one some attributes.
I would like one of the attributes to be placed in a seperate collection with another model, after I altered the data.
Now altering the data is not a problem and I know how to create the new object.
However I don't know what the best way to go is with:
where to alter the data
how to get it in my collection.
I tried several things from sending the complete attribute to the collection, where I do I have a parse which should give back a new object.
This however fails.
I also tried to do the same with the model and a parse.
Then I tried to just return the object in the 'url' section of the collection.
but as expected this does not work either. After which I tried to stringify the object to json, but this didn't work either.
So now I am thinking it might be the best way to start altering the data in Model A, after the data has ben received, and then push or add the data to the collection.
Maybe anyone else has a better idea of doing this?
And how do I know the data is in Model A, so I can start running the script?
First try in the collection: by URL
SatPhotoDataCollection = Backbone.Collection.extend({
model: SatPhotoDataModel,
url: function() {
var obj = satPhotoModel.attributes.Layer;
var layerNames = {};
for (var i = 0; i < obj.length; i+=1) {
//do some massive things filling layerNames
}
return layerNames;
}
});
Same thing, not using URL but parse, sending the 'satPhotoModel.attributes.Layer' to the collection using:
SatPhotoDataCollection.add(satPhotoModel.attributes.Layer);
SatPhotoDataCollection = Backbone.Collection.extend({
model: SatPhotoDataModel,
url: "",
parse: function(data) {
var layerNames = {};
for (var i = 0; i < data.length; i+=1) {
//do some massive things filling layerNames
}
return layerNames;
}
});
Of course I did the same in the model satPhotoDataModel (model B) in the URL and Parse, however I must the 'layerNames' I return is really a collection of 'layers', so hence the need for a collection ;)
Now in the satPhotoModel (model A) which will have the Layer in it's attributes I could also try something like:
SatPhotoModel = Backbone.Model.extend({
initialize: function() {
},
url: "http://geoservertest/geoserver/DIWADIS/ows?service=WMS&version=2.0.0&request=GetCapabilities",
sync: function(method, model, options) {
options.dataType = "xml";
options.crossDomain = true;
options.contentType = 'application/json; charset=utf-8';
return Backbone.sync(method, model, options);
},
//we are expecting an XML since we want a normal object, we do this with parsing
parse: function(data) {
var obj = $.xml2json(data);
this.parseLayers(obj.Capability.Layer);
return obj.Capability.Layer;
},
defaults: {
Abstract: "",
BoundingBox: {},
CRS: [],
Ex_GeographicBoundingBox: {},
Layer: [],
Tile: ""
},
parseLayers: function(data) {
var layerNames = {};
for (var i = 0; i < data.length; i+=1) {
//do some massive things filling layerNames
}
SatPhotoDataCollection.add(layerNames);
}
});
I am just doubting if that is really the correct way to go.
So any help and enlightenment would be awesome :-)
I need to have two url properties inside my Backbone.Collection.extend() because if a collection is fetched then I need to use a specific url if the collection gets a new model then I want to change the url
module.exports = MessagesCollection = Backbone.Collection.extend({
initialize: function(models, options) {
this.id = options.id;
},
url: function() {
if (fetch method is called) {
return '/api/messages/' + this.id;
} else {
// here if a model is being added?
return '/api/messages'
}
},
model: MessageModel
});
The reason for this is because I only want to pull down the models from the server based on the user.
var me = new MeModel();
me.fetch({
success: function(response) {
App.data.me = me;
var messages = new MessagesCollection([], { id: response.get('user_id') });
messages.fetch({
success: function() {
App.data.messages = messages;
App.core.vent.trigger('app:start');
}
});
}
});
When the user creates a new model within the app I want it to go into the main collection?
Does this mean I should create a sub collection based on the main collection somehow?
Edit:
My create looks like this somewhere else in the app window.App.data.messages.create(Message); I am thinking maybe I could write something like
var me = new MeModel();
me.fetch({
success: function(response) {
App.data.me = me;
var messages = new MessagesCollection([], { id: response.get('user_id') });
var allMessages = new MessagesCollection();
messages.fetch({
success: function() {
App.data.messages = messages;
App.data.allMessages = allMessages;
App.core.vent.trigger('app:start');
}
});
}
});
Then create window.App.data.allMessages.create(Message);> It sounds like it can cause problems IDK any ideas?
Edit:
The above worked but I had to create a new Backbone.Collection.extend() passing the same model but just writing it like
var Backbone = require('backbone'),
MessageModel = require('../models/message');
module.exports = AllMessagesCollection = Backbone.Collection.extend({
model: MessageModel,
url: '/api/messages'
});
So let me really break this question down, is this solution problematic. What is the best way to do this? The worst thing I can think of is bandwidth, using this method I would constantly be sending requests!
If you need to use different url only when create new model you can override collection.create method:
var MessagesCollection = Backbone.Collection.extend({
initialize: function(models, options) {
this.id = options.id;
},
url: function() {
return '/api/messages/' + this.id;
},
create: function(model, options){
var extendedOptions = _.extend(options || {}, {url: '/api/messages'});
return this.constructor.__super__.create.call(this, model, extendedOptions);
}
});
In every authenticated requests (GET, POST, etc) of my Backbone/Marionette application I must to attach an accessToken.
I store this accessToken and expireDate in the localStorage.
To check if the accessToken is expired I call this method: user.checkToken().
If is expired, the method renew the accessToken with a POST request to my backend.
Where should I put this check? I mean, in which part of the application?
Should I rewrite my on Backbone.sync method or use ajax.setup "beforeSend" ?
Thanks in advance for your advices/idea.
Backbone uses jQuery (see the note for a solution that may work with Zepto) for ajax requests, so you can use (as suggested by Edward) jQuery.ajaxPrefilter.
I did a little test for this task, let me know if there's any problem:
function tokenIsExpired() {
return true;
}
function createPromiseFunction(method, jqXHRsource, jqXHR) {
return function() {
jqXHRsource[method] = function(f) {
if (f) {
jqXHR[method] = function() {
f.apply(this, arguments);
};
}
return this;
};
}
}
function updateToken() {
return $.ajax({
url: '',
method: 'GET',
data: {some:'data', here:'yes'},
success: function() {
// update the token sir
console.log('token call done')
},
skipTokenCheck: true // required
});
}
$.ajaxPrefilter(function( options, originalOptions, jqXHR ) {
/*
* check if token is expired every time a new ajax request is made
* if it is expired, aborts the current requests, updated the token
* and eventually does the original request again.
*/
if (!options.skipTokenCheck && tokenIsExpired()) {
// at this point no callback should have be added to the promise object
var methodsNames = [
'done',
'always',
'fail',
'progress',
'then'
];
var methods = {};
// copy the callbacks when they're added to the old request
for (var i = 0; i < methodsNames.length; i++) {
var name = methodsNames[i];
createPromiseFunction(name, jqXHR, methods)();
};
jqXHR.abort();
// TODO: error checks
updateToken().done(function() {
console.log('done');
var newReq = $.ajax($.extend(originalOptions, {skipTokenCheck: true}));
for (var i = 0; i < methodsNames.length; i++) {
var name = methodsNames[i];
var f = methods[name];
if (f) {
newReq[name](f);
}
};
});
}
});
var p = $.get('.');
p.done(function() { console.log(arguments); }).fail(function() {
console.log('fail');
});
Looks like that ajaxPrefilter doesn't work with Zepto. Alternatively you can use the ajaxBeforeSend event.
Returning false in the beforeSend function will cancel the request.
Should be easy to adapt the code I posted above.
Overwrite your model's sync() function and do whatever you need to do.. Something like:
var MyModel = Backbone.Model.extend({
sync: function() {
// Put your code here
Backbone.Model.prototype.sync.apply(this, arguments);
}
});
Edit #1:
Not sure where you get user (as well as other variables) from but here it is:
var MyModel = Backbone.Model.extend({
sync: function() {
user.checkToken().done(_.bind(function(){
Backbone.Model.prototype.sync.apply(this, [ method, model, options ]);
});
}, this);
});
I'm trying to build a custom StackOverflow badge using JSONP and MooTools. Here is the code:
new Request.JSONP('http://stackoverflow.com/users/flair/166325.json', {
onComplete: function(data) {
console.log(data);
}
}).request();
However, I always get back this message:
RequestJSONPrequest_maprequest_0 is not defined
I'm wondering if this is a problem with the response from StackOverflow since requests to other services with JSONP work fine for me.
found a way around it: http://www.jsfiddle.net/CRdr6/1/
by passing on callbackKey: "callback=myfunc&foo" to the Request.JSONP class (it's not escaped properly) you can use myfunc as a global function to handle the callback and go around the stripped .
Request.stackoverflow = new Class({
Extends: Request.JSONP,
options: {
log: true,
url: "http://stackoverflow.com/users/flair/{user}.json",
callbackKey: "callback=myfunc&foo"
},
initialize: function(user, options) {
this.parent(options);
this.options.url = this.options.url.substitute({user: user});
},
success: function(data, script) {
this.parent(data, script);
}
});
window.myfunc = function(data) {
console.log(data);
};
new Request.stackoverflow(166325).send();
I ended up creating the function that StackOverflow ends up calling (without the dots):
var StackOverflow = Class.refactor(JsonP, {
getScript: function(options) {
var index = Request.JSONP.counter;
var script = this.previous(options);
eval("RequestJSONPrequest_maprequest_" + index + " = Request.JSONP.request_map['request_' + index];");
return script;
}
});