Clicking the Back button changes the URL but not the page, for pages with AJAX loaded sections and using Backbone.history.navigate - ajax

I'm using a Backbone Router with Backbone.history.start({pushState: true, root: "/"}); and I'm loading sections of pages through AJAX and use Backbone.history.navigate(url, true); to make sure the url shown by the browser points to the corresponding section.
For instance, I load a page with the relative url "/username/profile", then load a section on that page ('Favorite Books') by clicking on a tab and triggering an AJAX request, and then change the url to "/username/favorite_book".
The problem is that if I go back (with the Back button) to a previous section from the one loaded through ajax, the page content does not change even though the url changes.
I have seen previous posts talking about Ajax Browser History, but I would like to know what should I do in the context of Backbone? I could not find a clear explanation of the issue and how to solve it.
To be precise, what should I add to the function I trigger when clicking on the tab of a section to be loaded with ajax? My aim is to change the URL and the page (go back to state before AJAX request) when using the Back button. I'm currently doing as follows:
RenderSection: function(event) {
var data = '';
var url = $(event.currentTarget).attr("href");
$.post(url, data, function(data){
$(".ajax_section").html(data);
var protocol = this.protocol + '//';
// Ensure the protocol is not part of URL, meaning its relative.
if (url && url.slice(protocol.length) !== protocol) {
Backbone.history.navigate(url, true);
}
});
return false;
},

Turns out I was not using Backbone correctly. Here is what I ended up using and it works great!
In my Backbone View, I created 2 methods: the first is triggered when a link (an anchor) with class="ajax_enabled" is clicked, while the second is triggered by a Backbone Events trigger included in the Router's action. The Backbone View methods look as follows:
events: {
'click a.ajax_enabled': 'NavigateToUrl'
}
initialize: function() {
EventAggregator.on("render:route", this.RenderAjax, this);
},
NavigateToUrl: function(event) {
var url = $(event.currentTarget).attr("href");
var protocol = this.protocol + '//';
// Ensure the protocol is not part of URL, meaning its relative.
if (url && url.slice(protocol.length) !== protocol) {
Backbone.history.navigate(url, true);
}
return false;
},
RenderAjax: function(route) {
var data = '';
var url = window.location.pathname + window.location.search;
$.post(url, data, function(data){
$(".ajax_section").html(data);
});
}
My Backbone Router handles the call from Backbone.history.navigate(url, true); and triggers the event to update the view through the default action, as follows:
window.EventAggregator = _.extend({}, Backbone.Events);
var Router = Backbone.Router.extend({
initialize: function(options) {
var router = this;
Backbone.history.start({pushState: true, root: "/", silent: true});
},
routes: {
'' : 'defaultAction',
'*route': 'defaultAction'
},
defaultAction: function(route) {
if(typeof(route)==='undefined') {
route = '';
}
EventAggregator.trigger("render:route", route);
}
});
return Router;
For reference, I found this other answer helpful, about using events to trigger methods in the View from the Router.

Related

How to download the model as a JSON file?

My model is held in a JavaScript object on the client side, where the user can edit its properties via the UI controls. I want to offer the user an option to download a JSON file representing the model they're editing. I'm using MVC core with .net 6.
What I've tried
Action method (using Newtonsoft.Json to serialize the model to JSON):
public IActionResult Download([FromForm]SomeModel someModel)
{
var json = JsonConvert.SerializeObject(someModel);
var characters = json.ToCharArray();
var bytes = new byte[characters.Length];
for (var i = 0; i < characters.Length; i++)
{
bytes[i] = (byte)characters[i];
}
var stream = new MemoryStream();
stream.Write(bytes);
stream.Position = 0;
return this.File(stream, "APPLICATION/octet-stream", "someFile.json");
}
Code in the view to call this method:
<button class="btn btn-primary" onclick="download()">Download</button>
And the event handler for this button (using jQuery's ajax magic):
function download() {
$.ajax({
url: 'https://hostname/ControllerName/Download',
method: 'POST',
data: { someModel: someModel },
success: function (data) {
console.log('downloading', data);
},
});
}
What happened
The browser console shows that my model has been posted to the server, serialized to JSON and the JSON has been returned to the browser. However no file is downloaded.
Something else I tried
I also tried a link like this to call the action method:
#Html.ActionLink("Download", "Download", "ControllerName")
What happened
This time a file was downloaded, however, because ActionLink can only make GET requests, which have no request body, the user's model isn't passed to the server and instead the file which is downloaded represents a default instance of SomeModel.
The ask
So I know I can post my model to the server, serialize it to JSON and return that JSON to the client, and I know I can get the browser to download a JSON-serialized version of a model, but how can I do both in the same request?
Edit: What I've done with the answer
I've accepted Xinran Shen's answer, because it works as-is, but because I believe that just copying code from Stack Overflow without understanding what it does or why isn't good practice, I did a bit of digging and my version of the saveData function now looks like this:
function saveData(data, fileName) {
// Convert the data to a JSON string and store it in a blob, a file-like
// object which can be downloaded without it existing on the server.
// See https://developer.mozilla.org/en-US/docs/Web/API/Blob
var json = JSON.stringify(data);
var blob = new Blob([json], { type: "octet/stream" });
// Create a URL from which the blob can be downloaded - see
// https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL
var url = window.URL.createObjectURL(blob);
// Add a hidden hyperlink to the page, which will download the file when clicked
var a = document.createElement("a");
a.style = "display: none";
a.href = url;
a.download = fileName;
document.body.appendChild(a);
// Trigger the click event on the hyperlink to download the file
a.click();
// Release the blob's URL.
// Browsers do this when the page is unloaded, but it's good practice to
// do so as soon as it's no longer needed.
window.URL.revokeObjectURL(url);
// Remove the hidden hyperlink from the page
a.remove();
}
Hope someone finds this useful
First, Your code is right, You can try to access this method without ajax, You will find it can download file successfully,But You can't use ajax to achieve this, because JavaScript cannot interact with disk, you need to use Blob to save the file. change your javascript like this:
function download() {
$.ajax({
url: 'https://hostname/ControllerName/Download',
method: 'Post',
data: { someModel: someModel },,
success: function (data) {
fileName = "my-download.json";
saveData(data,fileName)
},
});
}
var saveData = (function () {
var a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
return function (data, fileName) {
var json = JSON.stringify(data),
blob = new Blob([json], {type: "octet/stream"}),
url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
};
}());
I think you may need FileStreamResult, also you need to set the MIME type to text file or json file.
// instead of this
return this.File(stream, "APPLICATION/octet-stream", "someFile.json");
// try this
return new FileStreamResult(stream, new MediaTypeHeaderValue("text/plain"))
{
FileDownloadName = "someFile.txt"
};
// or
return new FileStreamResult(stream, new MediaTypeHeaderValue("application/json"))
{
FileDownloadName = "someFile.json"
};
Reference: https://www.c-sharpcorner.com/article/fileresult-in-asp-net-core-mvc2/

session.setAttribute only updates after page reload

I have an ajax request which invokes GetTierNamesServlet:
$('#application').change(function() {
$.ajax({
url : 'GetTierNamesServlet',
data : {
name : $('#application').find(":selected").text()
},
type : 'get',
cache : false,
success : function(data) {
},
error : function() {
alert('error');
}
}).done(function() {
var test = '<c:out value="${tiers}" />';
alert(test)
})
});
GetTierNamesServlet saves 'tiers' to a session attribute as follows and forwards back to the same page (index.html).
HttpSession session = request.getSession(false);
session.setAttribute("tiers", tiers);
getServletContext().getRequestDispatcher("/index.jsp").forward(request, response);
When alert(test) is called, it alerts the selected tiers from the previous time the ajax request was processed.
The session attribute 'tiers' always seems to "lag" one refresh behind.
What am I doing incorrectly here? I would expect that by placing the alert within the .done portion of the ajax request it would wait the asynchronous call to return before doing something.
This fragment of JavaScript:
var test = '<c:out value="${tiers}" />';
is rendered with the value of ${tiers} before your servlet is called. If you inspect the HTML on the page you will likely find something like:
...
}).done(function() {
var test = 'null'; // or some other "old" value
alert(test)
})
the JSP content is translated to HTML and sent to the browser (page 1)
some event on page 1 causes the JavaScript to be executed
the AJAX call results in the page being rendered again and returned to the browser (page 2)
the JavaScript in page 1 finishes executing via the .done(...) function.
You AJAX call is returning a page when it should probably return a JSON fragment containing your tiers content which will then be consumed by the .done function.

SignalR not working with DelegatingHandler

We have a delegating-handler that catches requests with a certain url prefix, and then reroutes them behind the firewall with the fed auth cookie attached...
This is working for our WebApi layer, but SingalR is firing off requests on its own while it is trying to connect that doesn't follow the pattern... I can't figure out how to force it to use the proper url prefix.
This is the url that is generated from the post request when it is trying to do long-polling: https://localhost:44330/signalr/connect?transport=longPolling&
See that it hasn't put the '/qsixlsignalr' into the url, which my delegating handler will be looking for.
var signalRBaseURL = "/qsixlsignalr"
$(function () {
// http://stackoverflow.com/questions/15467373/signalr-1-0-1-cross-domain-request-cors-with-chrome
$.support.cors = false;
var connection = $.hubConnection(signalRBaseURL);
var myHub = connection.createHubProxy('xlHub');
myHub.on('notify', function (message) {
alertsViewModel.refreshActiveCount(localStorage.getItem(PROJECT_ID));
if (window.location.pathname == '/' || window.location.pathname == '') {
alertsViewModel.refresh(localStorage.getItem(PROJECT_ID));
}
toastr.success(message);
});
connection.disconnected(function () {
setTimeout(function () {
connection.start();
}, 3000);
});
connection.logging = true;
connection.start();
});
If I remember correctly you need to tell SignalR explicitly that you don't want to use the the default url
var connection = $.hubConnection(signalRBaseURL, { useDefaultPath: false });

a file upload progress bar with node (socket.io and formidable) and ajax

I was in the middle of teaching myself some Ajax, and this lesson required building a simple file upload form locally. I'm running XAMPP on windows 7, with a virtual host set up for http://test. The solution in the book was to use node and an almost unknown package called "multipart" which was supposed to parse the form data but was crapping out on me.
I looked for the best package for the job, and that seems to be formidable. It does the trick and my file will upload locally and I get all the details back through Ajax. BUT, it won't play nice with the simple JS code from the book which was to display the upload progress in a progress element. SO, I looked around and people suggested using socket.io to emit the progress info back to the client page.
I've managed to get formidable working locally, and I've managed to get socket.io working with some basic tutorials. Now, I can't for the life of me get them to work together. I can't even get a simple console log message to be sent back to my page from socket.io while formidable does its thing.
First, here is the file upload form by itself. The script inside the upload.html page:
document.getElementById("submit").onclick = handleButtonPress;
var httpRequest;
function handleResponse() {
if (httpRequest.readyState == 4 && httpRequest.status == 200) {
document.getElementById("results").innerHTML = httpRequest.responseText;
}
}
function handleButtonPress(e) {
e.preventDefault();
var form = document.getElementById("myform");
var formData = new FormData(form);
httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = handleResponse;
httpRequest.open("POST", form.action);
httpRequest.send(formData);
}
And here's the corresponding node script (the important part being form.on('progress')
var http = require('http'),
util = require('util'),
formidable = require('formidable');
http.createServer(function(req, res) {
if (req.url == '/upload' && req.method.toLowerCase() == 'post') {
var form = new formidable.IncomingForm(),
files = [],
fields = [];
form.uploadDir = './files/';
form.keepExtensions = true;
form
.on('progress', function(bytesReceived, bytesExpected) {
console.log('Progress so far: '+(bytesReceived / bytesExpected * 100).toFixed(0)+"%");
})
.on('file', function(name, file) {
files.push([name, file]);
})
.on('error', function(err) {
console.log('ERROR!');
res.end();
})
.on('end', function() {
console.log('-> upload done');
res.writeHead(200, "OK", {
"Content-Type": "text/html", "Access-Control-Allow-Origin": "http://test"
});
res.end('received files: '+util.inspect(files));
});
form.parse(req);
} else {
res.writeHead(404, {'content-type': 'text/plain'});
res.end('404');
}
return;
}).listen(8080);
console.log('listening');
Ok, so that all works as expected. Now here's the simplest socket.io script which I'm hoping to infuse into the previous two to emit the progress info back to my page. Here's the client-side code:
var socket = io.connect('http://test:8080');
socket.on('news', function(data){
console.log('server sent news:', data);
});
And here's the server-side node script:
var http = require('http'),
fs = require('fs');
var server = http.createServer(function(req, res) {
fs.createReadStream('./socket.html').pipe(res);
});
var io = require('socket.io').listen(server);
io.sockets.on('connection', function(socket) {
socket.emit('news', {hello: "world"});
});
server.listen(8080);
So this works fine by itself, but my problem comes when I try to place the socket.io code inside my form.... I've tried placing it anywhere it might remotely make sense, i've tried the asynchronous mode of fs.readFile too, but it just wont send anything back to the client - meanwhile the file upload portion still works fine. Do I need to establish some sort of handshake between the two packages? Help me out here. I'm a front-end guy so I'm not too familiar with this back-end stuff. I'll put this aside for now and move onto other lessons.
Maybe you can create a room for one single client and then broadcast the percentage to this room.
I explained it here: How to connect formidable file upload to socket.io in Node.js

Stop Duplicate Ajax Submisions?

I am wondering what is the best way to stop duplciate submissions when using jquery and ajax?
I come up with 2 possible ways but not sure if these are the only 2.
On Ajax start disable all buttons till request is done. 2 problems I see with this though is I use jquery model dialog so I don't know how easy it would be to disable those button as I not sure if they have id's. Second I if the the request hangs the user has really no way to try again since all the buttons are disabled.
I am looking into something called AjaxQueue at this time I have no clue if it is what I need or how it works since the site where the plugin is apparently down for maintenance.
http://docs.jquery.com/AjaxQueue
Edit
I think this is a spin off of what I was looking at.
http://www.protofunc.com/scripts/jquery/ajaxManager/
The only problem I see with this ajaxManager is that I think I have to change all my $.post, $.get and $.ajax ones to their type.
But what happens if I need a special parameter from $.ajax? Or that fact I like using .post and .get.
Edit 2
I think it can take in all $.ajax options. I am still looking into it. However what I am unsure about now is can I use the same constructor for all requests that will use the same options.
First you have to construct/configure a new Ajaxmanager
//create an ajaxmanager named someAjaxProfileName
var someManagedAjax = $.manageAjax.create('someAjaxProfileName', {
queue: true,
cacheResponse: true
});
Or do I have to make the above every single time?
How about setting a flag when the user clicks the button? You will only clear the flag when the AJAX request completes successfully (in complete, which is called after the success and error callbacks), and you will only send an AJAX request if the flag is not set.
Related to AJAX queuing there is a plugin called jQuery Message Queuing that is very good. I've used it myself.
var requestSent = false;
jQuery("#buttonID").click(function() {
if(!requestSent) {
requestSent = true;
jQuery.ajax({
url: "http://example.com",
....,
timeout: timeoutValue,
complete: function() {
...
requestSent = false;
},
});
}
});
You can set a timeout value for long-running requests (value is in milliseconds) if you think your request has a possibility of hanging. If an timeout occurs, the error callback is called, after which the complete callback gets called.
You could store an active request in a variable, then clear it when there's a response.
var request; // Stores the XMLHTTPRequest object
$('#myButton').click(function() {
if(!request) { // Only send the AJAX request if there's no current request
request = $.ajax({ // Assign the XMLHTTPRequest object to the variable
url:...,
...,
complete: function() { request = null } // Clear variable after response
});
}
});
EDIT:
One nice thing about this, is that you could cancel long running requests using abort().
var request; // Stores the XMLHTTPRequest object
var timeout; // Stores timeout reference for long running requests
$('#myButton').click(function() {
if(!request) { // Only send the AJAX request if there's no current request
request = $.ajax({ // Assign the XMLHTTPRequest object to the variable
url:...,
...,
complete: function() { timeout = request = null } // Clear variables after response
});
timeout = setTimeout( function() {
if(request) request.abort(); // abort request
}, 10000 ); // after 10 seconds
}
});
$.xhrPool = {};
$.xhrPool['hash'] = []
$.ajaxSetup({
beforeSend: function(jqXHR,settings) {
var hash = settings.url+settings.data
if ( $.xhrPool['hash'].indexOf(hash) === -1 ){
jqXHR.url = settings.url;
jqXHR.data = settings.data;
$.xhrPool['hash'].push(hash);
}else{
console.log('Duplicate request cancelled!');
jqXHR.abort();
}
},
complete: function(jqXHR,settings) {
var hash = jqXHR.url+jqXHR.data
if (index > -1) {
$.xhrPool['hash'].splice(index, 1);
}
}
});

Resources