We are trying to modify an existing script which uses backbone.js to fetch JSON from a URL and render it in a defined way on screen.
Earlier the script was pointing to an external PHP file to fetch the JSON from it.
url: function () {
var ajaxValue = document.getElementById('ajax').value;
if(ajaxValue==0){
return this.options.apiBase + '/liveEvents.json';
} else {
var eventDate = document.getElementById('timestamp').value;
return this.options.apiBase + '/ajax.php?eventDate='+eventDate;
}
},
But now we are trying to omit the requirement of PHP and get JSON purely using Javascript. For this, we created a JS function fetch_data_set(), that returns proper JSON
var ArrayMerge = array1.concat(array2,array3,array4);
return JSON.stringify(ArrayMerge);
So our question is, how can we feed this JSON to backbone instead of using an external URL. Because if we do this (which is obviously wrong):
url: function () {
var ajaxValue = document.getElementById('ajax').value;
if(ajaxValue==0){
var data_set = fetch_data_set();
return data_set;
}
},
It throws error: Error: A "url" property or function must be specified
The main key is to extend Backbone.sync instead of url() method, so you could use this way to fetch your models in any kind of model, and you could do something similar like this link:
https://github.com/huffingtonpost/backbone-fixtures/blob/master/backbone-fixtures.js
Backbone.Model contains a sync() function able to load JSON data from an url. sync() uses the url() function to determine from where it should fetch data. (Note : sync() is called under-the-hood by save(), fetch() and destroy())
The trick here is that you should stop overriding url() and reimplement sync() directly instead, cf. http://backbonejs.org/#Model-sync
Here is an example :
// specialized version to be used with a store.js - like object
sync: function(method, model, options) {
console.log("sync_to_store begin('"+method+"',...) called with ", arguments);
var when_deferred = when.defer();
var id = this.url();
if(method === "read") {
if(typeof id === 'undefined')
throw new Error("can't fetch without id !");
var data = model.store_.get(id);
// apply fetched data
model.set(data);
when_deferred.resolve( [model, undefined, options] );
}
else if(method === "create") {
// use Backbone id as server id
model.id = model.cid;
model.store_.set(id, model.attributes);
when_deferred.resolve( [model, undefined, options] );
}
else if(method === "update") {
if(typeof id === 'undefined')
throw new Error("can't update without id !");
model.store_.set(id, model.attributes);
when_deferred.resolve( [model, undefined, options] );
}
else if(method === "delete") {
if(typeof id === 'undefined')
throw new Error("can't delete without id !");
model.store_.set(id, undefined);
model.id = undefined;
when_deferred.resolve( [model, undefined, options] );
}
else {
// WAT ?
}
console.log("sync_to_store end - Current changes = ", model.changed_attributes());
return when_deferred.promise;
}
Note 1 : API is slightly different from vanilla Backbone since I return
a when promise
Note 2 : url() is still used, as an id
Related
Say if I have small function that takes a Request object as an argument, and calls the fetch() API.
Except I always want to append something to the url, such as ?foo=bar. I'm curious what the best way would be to go about that.
Example:
function fetchFoo(request) {
request.url += '?foo=bar';
return fetch(request);
}
The issue I have is that this won't work. The Fetch API specification states that the url property read-only.
Is there a way to work around this? I'm thinking I might need to construct an all-new Request object, but I'm unsure what a clever way is to inherit all the options from the previous Request object.
Note that I am able to override any other property by using this syntax:
var originalRequest = new Request('/url');
var overriddenRequest = new Request(originalRequest, { method: 'POST' });
Although it wasn't clear from the docs, it seems that the second init parameter takes precedence over the values passed via the originalRequest parameter. I just can't seem to come up with a way to do this for the url as well.
You could leverage the keys that are on the Request.prototype to build a new Request object in just a few lines.
function newRequest(input, init) {
var url = input;
if (input instanceof Request) {
url = mutateUrl(input.url);
init = init || {};
Object.keys(Request.prototype).forEach(function (value) {
init[value] = input[value];
});
delete init.url;
return input.blob().then(function (blob) {
if (input.method.toUpperCase() !== 'HEAD' && input.method.toUpperCase() !== 'GET' && blob.size > 0) {
init.body = blob;
}
return new Request(url, init);
})
} else {
url = mutateUrl(url);
}
return new Request(url, init);
}
Note the special case for body discussed in this answer.
I am having an issue trying to improve the functionality of my ajax functions by adding arguments to define what to do with the returned result.
So what i am trying to implement is a way to tell the function to append the result to a given element by its id. But i am having difficulty understanding how to add this functionality.
This is my current code:
var ajax = new function(){
var self = this;
self.x = function() {
if (typeof XMLHttpRequest !== 'undefined') {
return new XMLHttpRequest();
}
};
self.send = function(url, callback, method, data, sync) {
var x = self.x();
x.open(method, url, sync);
x.onreadystatechange = function() {
if (x.readyState == 4) {
callback(x.responseText)
}
};
if (method == 'POST') {
x.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
}
x.send(data)
};
self.get = function(url, data, callback, sync) {
var query = [];
for (var key in data) {
query.push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
}
self.send(url + (query.length ? '?' + query.join('&') : ''), callback, 'GET', null, sync)
};
};
I then make an ajax request like so:
//get the html file, and then call a function
ajax.get(dir.layout+'login.html',false,
function(){
elements.addTemplate(data,'parent',true);
});
In Chrome the xhr shows the correct data and contents, so i know that part works. In my elements.loadTemplate function i have these three lines with their actual values:
elements.addtemplate(data,div_id,append){
console.log(data); //shows:
console.log(div_id); //shows string: parent
console.log(append); //shows: true
}
Now the issue is data is blank, when i want it to contain the contents of the HTML file that I just requested (in this case login.html). I am wondering why it would show up as blank and how i can fix it?
your data is undefined because your callback doesn't accept a parameter
try this:
ajax.get(dir.layout+'login.html',false,
function(data){ // <=== data
elements.addTemplate(data,'parent',true);
});
I have json data with "tagged" values (from a jsonp source):
{"foo": "#duration:8542"}
which I can parse on-the-fly by passing a function as the second argument to JSON.parse:
dk.json = {
parse: function (s) {
return JSON.parse(s, function (key, val) {
if (typeof val === 'string' && val[0] === '#') {
var colonpos = val.indexOf(':');
if (colonpos > 1) {
var tag = val.slice(0, colonpos + 1);
switch (tag) {
case dk.Date.tag: return dk.Date.create(val);
case dk.Duration.tag: return dk.Duration.create(val);
}
}
}
return val;
});
},
//...
};
but how can I plug this parsing function into jQuery.ajax()? Something more sensible than:
success: function (data) {
data = dk.json.parse(JSON.stringify(data));
...
dataFilter, and especially converters looked promising:
$.ajax({
dataType: 'jsonp',
converters: {
'text json': dk.json.parse
},
// ...
});
but that doesn't get called at all (dataFilter gets called, but with the data parameter set to undefined).
Where am I going wrong?
[Edit:]
I know I can write a traversal function that walks the JSON object returned by jQuery, eg:
function untag(val) {
if (typeof val === 'string' && val[0] === '#') {
var colonpos = val.indexOf(':');
if (colonpos > 1) {
var tag = val.slice(0, colonpos + 1);
switch (tag) {
case dk.Date.tag: return dk.Date.create(val);
case dk.Duration.tag: return dk.Duration.create(val);
}
}
}
return val;
}
var untag_json = function (jsonobj) {
var _traverse = function _traverse(obj, result) {
var value;
for (var attr in obj) {
value = obj[attr];
if (value && typeof value === 'object') {
result[attr] = _traverse(value, {});
} else {
result[attr] = untag(value);
}
}
return result;
};
return _traverse(jsonobj, {});
};
and then call it in the success handler:
success: function (data) {
data = untag_json(data);
...
but that seems like a lot of unnecessary work.. Is there no way to use the converters parameter to $.ajax to get access to the unparsed (i.e. text) json source?
There actually isn't any JSON parsing in a JSONP request (src), which can seem counter intuitive. What is happening is the string that is returning from the JSONP endpoint is evaluated as JavaScript (with a reference to a function that is defined (or added in dynamically) in the DOM making the JSONP request like this:
_callback({'foo':'#duration:8524'});
If you wanted to use your function you would need to make the endpoint return a String like this:
_callback("{'foo':'#duration:8524'}");
then in the JSONP callback you could call JSON.parse(). JSON parse is a rather safe way to process JSON so if this was easier to reason about then it would be a fine approach.
Hi you need to set this header application/json in the response from server side then you can simply set dataType:json or dataType:jsonp then you will not need to stringify or parse the json. You then just get objects, properties or arrays from json.
For example : in php we use
$json_string = "{"foo": "#duration:8542"}";
$json = json_decode($json_string);
$foo = $json->foo;
echo $foo;//prints #duration:8542
In jquery you can do this:
sucess:function(response) {
var foo = response.foo;
console.log(foo);
}
Hope this helps
For mapping my backbone models to what I get from the server I am using a technique described on the GroupOn Dev blog: https://engineering.groupon.com/2012/javascript/extending-backbone-js-to-map-rough-api-responses-into-beautiful-client-side-models/
However, this only maps incoming data to the model.
I would like this to go both ways, so that when I save a model, it prepares the models attributes to match the servers model.
What would be the best solution to prepare the output of the model?
I've run into this exact same issue where my server response is completely different from what I am able to post. I discovered within the mechanics of the Backbone.sync object a way to that I could post to my server a custom JSON object in the following statement in Backbone.sync:
if (!options.data && model && (method == 'create' || method == 'update')) {
params.contentType = 'application/json';
params.data = JSON.stringify(model.toJSON());
}
sync evaluates if options.data does not exist then sets the params.data to the stringified model. The options.data check keyed me off. If that exists, sync will use that instead of the model. So given this, I overrode my model.save so could pass in an attributes hash that my server expects.
Here's how I overrode it:
save : function(key, value, options) {
var attributes = {}, opts = {};
//Need to use the same conditional that Backbone is using
//in its default save so that attributes and options
//are properly passed on to the prototype
if (_.isObject(key) || key == null) {
attributes = key;
opts = value;
} else {
attributes = {};
attributes[key] = value;
opts = options;
}
//In order to set .data to be used by Backbone.sync
//both opts and attributes must be defined
if (opts && attributes) {
opts.data = JSON.stringify(attributes);
opts.contentType = "application/json";
}
//Finally, make a call to the default save now that we've
//got all the details worked out.
return Backbone.Model.prototype.save.call(this, attributes, opts);
}
So how do you use this in your case? Essentially what you'll do is create a method that reverses the mapping and returns the resulting JSON. Then you can invoke save from your view or controller as follows:
getReversedMapping : function() {
ver reversedMap = {};
...
return reversedMap;
},
saveToServer : function() {
this._model.save(this.getReverseMapping, {
success : function(model, response) {
...
},
error : function(model, response) {
...
}
})
}
Since your overridden save automatically copies the JSON you pass in to options.data, Backbone.sync will use it to post.
The answer by Brendan Delumpa works, but it over-complicates things.
Don't do this in your save method. You don't want to copy over these parameter checks each time (and what if they somehow change in Backbone?).
Instead, overwrite the sync method in your model like this:
var MyModel = Backbone.Model.extend({
...,
sync: function (method, model, options) {
if (method === 'create' || method === 'update') {
// get data from model, manipulate and store in "data" variable
// ...
options.data = JSON.stringify(data);
options.contentType = 'application/json';
}
return Backbone.Model.prototype.sync.apply(this, arguments);
}
});
That's all there is to it when you need to "prepare" the data in a server-ready format.
After watching RailsCast #296 about Mercury Editor, I am trying to get the editor to redirect to a newly created resource.
I can already redirect on the client-side using JS and window.location.href=. But for a new resource, I cannot "guess" its URL on the client-side. I need it to be in the server response.
However, the problem is that I don't see the possibility of using the server response in the editor. No matter what the controller renders, the server response is discarded by Mercury instead of used as an argument to my function for mercury:saved.
Is there a way to get around this?
I was able to do this on update by sending a valid JSON string back. I would assume create works the same way. check firebug to make sure you're not getting an error in the jQuery.ajax call that Mercury uses.
posts_controller.rb
def mercury_update
post = Post.find(params[:id])
post.title = params[:content][:post_title][:value]
post.body = params[:content][:post_body][:value]
post.save!
render text: '{"url":"'+ post_path(post.slug) +'"}'
end
mercury.js:
jQuery(window).on('mercury:ready', function() {
Mercury.on('saved', function() {
window.location.href = arguments[1].url
});
});
note: I'm using friendly_id to slug my posts
Redirecting on the server side doesn't work because the save button is just an jQuery.ajax call:
// page_editor.js
PageEditor.prototype.save = function(callback) {
var data, method, options, url, _ref, _ref1,
_this = this;
url = (_ref = (_ref1 = this.saveUrl) != null ? _ref1 : Mercury.saveUrl) != null ? _ref : this.iframeSrc();
data = this.serialize();
data = {
content: data
};
if (this.options.saveMethod === 'POST') {
method = 'POST';
} else {
method = 'PUT';
data['_method'] = method;
}
Mercury.log('saving', data);
options = {
headers: Mercury.ajaxHeaders(),
type: method,
dataType: this.options.saveDataType,
data: data,
success: function(response) {
Mercury.changes = false;
Mercury.trigger('saved', response);
if (typeof callback === 'function') {
return callback();
}
},
error: function(response) {
Mercury.trigger('save_failed', response);
return Mercury.notify('Mercury was unable to save to the url: %s', url);
}
};
if (this.options.saveStyle !== 'form') {
options['data'] = jQuery.toJSON(data);
options['contentType'] = 'application/json';
}
return jQuery.ajax(url, options);
};
So your redirect is sent to the success callback, but the page doesn't actually re-render, as with any successful AJAX request. The author discusses overriding this very function here. It also looks like there might be some room to maneuver here by passing a callback function to save.
Btw, another way to do what #corneliusk suggests is:
render { json: {url: post_path(post.slug)} }
Either way, the response body is passed as an argument to the function in the mercury:saved callback.