35 Comments »
A lot of people seem to be asking how to do a file upload using AJAX. The short answer to this is that it cannot be done! That’s because it is not supported by today’s HTML / browser specs.
What you can do is just fake it
.
I’ve been using Wicket (Java Web Framework) regularly for web development. Recently I needed to be able to do a file upload inside a modal window. File uploading needs form submission in order to work so I faced a few problems:
- My modal window was implemented as a panel. I could not do a regular form submission as that would close (submit) my modal window. I’ve been suggested to implement the modal window as a page instead of the panel so that I could use Ajax submit but I really needed it the way it was.
- Ajax and multipart don’t cope.
- Whatever I tried, without regular submission, the file uploading field would not return the file name so it was impossible to get.
I searched the web and I finally found and implemented the solution. It involves using an iframe, but it’s pretty simple. Here’s the code if anyone else needs this:
public abstract class UploadIFrame extends WebPage {
private boolean uploaded = false;
private FileUploadField uploadField;
private String newFileUrl;
public UploadIFrame() {
add(new UploadForm("form"));
addOnUploadedCallback();
}
/**
* return the callback url when upload is finished
* @return callback url when upload is finished
*/
protected abstract String getOnUploadedCallback();
/**
* Called when the input stream has been uploaded and when it is available
* on server side
* return the url of the uploaded file
* @param upload fileUpload
*/
protected abstract String manageInputSream(FileUpload upload);
private class UploadForm extends Form {
public UploadForm(String id) {
super(id);
uploadField = new FileUploadField("file");
add(uploadField);
add(new AjaxLink("submit"){
@Override
public void onClick(AjaxRequestTarget target) {
target.appendJavascript("showProgressWheel()");
}
});
}
@Override
public void onSubmit() {
FileUpload upload = uploadField.getFileUpload();
newFileUrl = manageInputSream(upload);
//file is now uploaded, and the IFrame will be reloaded, during
//reload we need to run the callback
uploaded = true;
}
}
private void addOnUploadedCallback() {
//a hacked component to run the callback on the parent
add(new WebComponent("onUploaded") {
@Override
protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
if (uploaded) {
if (uploadField.getFileUpload() != null){
replaceComponentTagBody(markupStream, openTag,
"window.parent." + getOnUploadedCallback() + "('" +
uploadField.getFileUpload().getClientFileName() + "','" +
newFileUrl +"')");
}
uploaded = false;
}
}
});
}
}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns:wicket>
<head>
<link href = "../counters/css/style.css" type = "text/css" rel = "stylesheet"/>
<!--[if IE]>
<link href = "../counters/css/ie.css" type = "text/css" rel = "stylesheet"/>
<![endif]-->
<script type="text/javascript">
function showProgressWheel() {
document.images[0].style.display = 'block';
// delay the wheel a bit so it can locally shine
setTimeout(function() { document.forms[0].submit() }, 800);
return false;
}
</script>
</head>
<body>
<form wicket:id="form">
<table style="width:500px; margin-top:0px;">
<tr>
<td>
<input style="width:auto;" wicket:id="file" type="file"/>
</td>
<td>
<a wicket:id="submit">Change Avatar</a>
</td>
<td>
<img src="images/indicator.gif" style="display:none"/>
</td>
</tr>
</table>
</form>
<script wicket:id="onUploaded" type="text/javascript"></script>
</body>
</html>
public abstract class UploadPanel extends Panel {
private InlineFrame uploadIFrame = null;
public UploadPanel(String id) {
super(id);
addOnUploadedCallback();
setOutputMarkupId(true);
}
/**
* Called when the upload load is uploaded and ready to be used
* Return the url of the new uploaded resource
* @param upload {@link FileUpload}
*/
public abstract String onFileUploaded(FileUpload upload);
/**
* Called once the upload is finished and the traitment of the
* {@link FileUpload} has been done in {@link UploadPanel#onFileUploaded}
* @param target an {@link AjaxRequestTarget}
* @param fileName name of the file on the client side
* @param newFileUrl Url of the uploaded file
*/
public abstract void onUploadFinished(AjaxRequestTarget target, String filename, String newFileUrl);
@Override
protected void onBeforeRender() {
super.onBeforeRender();
if (uploadIFrame == null) {
// the iframe should be attached to a page to be able to get its pagemap,
// that's why i'm adding it in onBeforRender
addUploadIFrame();
}
}
/**
* Create the iframe containing the upload widget
*
*/
private void addUploadIFrame() {
IPageLink iFrameLink = new IPageLink() {
@Override
public Page getPage() {
return new UploadIFrame() {
@Override
protected String getOnUploadedCallback() {
return "onUpload_" + UploadPanel.this.getMarkupId();
}
@Override
protected String manageInputSream(FileUpload upload) {
return UploadPanel.this.onFileUploaded(upload);
}
};
}
@Override
public Class<UploadIFrame> getPageIdentity() {
return UploadIFrame.class;
}
};
uploadIFrame = new InlineFrame("upload", getPage().getPageMap(), iFrameLink);
add(uploadIFrame);
}
/**
* Hackie method allowing to add a javascript in the page defining the
* callback called by the innerIframe
*
*/
private void addOnUploadedCallback() {
final OnUploadedBehavior onUploadBehavior = new OnUploadedBehavior();
add(onUploadBehavior);
add(new WebComponent("onUploaded") {
@Override
protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
// calling it through setTimeout we ensure that the callback is called
// in the proper execution context, that is the parent frame
replaceComponentTagBody(markupStream, openTag,
"function onUpload_" + UploadPanel.this.getMarkupId() +
"(clientFileName, newFileUrl) {window.setTimeout(function() { " +
onUploadBehavior.getCallback() + " }, 0 )}");
}
});
}
private class OnUploadedBehavior extends AbstractDefaultAjaxBehavior {
public String getCallback() {
return generateCallbackScript(
"wicketAjaxGet('" + getCallbackUrl(false) +
"&amp;newFileUrl=' + encodeURIComponent(newFileUrl)" +
" + '&amp;clientFileName=' + encodeURIComponent(clientFileName)").toString();
}
@Override
protected void respond(AjaxRequestTarget target) {
UploadPanel.this.onUploadFinished(target, getRequest().getParameter("clientFileName"), getRequest().getParameter("newFileUrl"));
}
};
}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns:wicket>
<wicket:panel>
<!-- I put the callback snippet at the body so that is rendered for each panel instead of once -->
<script wicket:id="onUploaded" type="text/javascript"></script>
<iframe wicket:id="upload" frameborder="0" style="height:60px; width:500px; overflow:hidden"></iframe>
</wicket:panel>
</html>
add(new UploadPanel("myUpload"){
@Override
public String onFileUploaded(FileUpload upload) {
if (upload != null){
if(upload.getSize() > 1000000) {
return "The Picture Is Too Big !";
} else {
try {
jdbcSignUp.changeAvatarFor(((WebSession)getSession()).getUser().getUsername(), upload.getInputStream(), (int)upload.getSize());
//save on server side here
//and return the url of the saved file
return "The Picture Has Been Uploaded";
} catch (IOException e) {
log.error("Exception:", e);
}
}
}
return "";
}
@Override
public void onUploadFinished(AjaxRequestTarget target, String filename, String newFileUrl) {
//when upload is finished, will be called
messageLabel.setModelObject(newFileUrl);
target.addComponent(messageLabel);
avatarImage.setImageResource(new MyBlobImageResource(((WebSession)getSession()).getUser().getUsername()));
target.addComponent(avatarImage);
((BasePage)parent).onAvatarChanged(target);
}
});
Most credit should go to Vincent Demay, although his code contains errors.
Filed under: Programming by admin