Recipe: Ajax file upload using FormData API

116 views
Skip to first unread message

Robert Marcano

unread,
Feb 8, 2013, 2:10:45 PM2/8/13
to lif...@googlegroups.com
I implemented a file upload field that use the modern FormData and File API

There is already a documented method to do this using iframes at
https://www.assembla.com/spaces/liftweb/wiki/AJAX_File_Upload if you
need support for older browsers. I am lucky, I can ignore old IE
releases because this is an intranet application (the FormData API is
supported on Explorer 10) and avoid using iframes. What I did was:

1. Implement a new JSArtifacts based on JQueryArtifacts. This new
implementation override the ajax method with code that adds processData
= false and contentType = false when the data to send is of the FormData
kind, this ensure that JQuery doesn't transform the object to a String
and set the wrong content type

========= Start MyJSArtifacts =========
object MyJSArtifacts extends JQueryArtifacts {
override def ajax(data: AjaxInfo): String = {
"var s = " + toJson(data, S.contextPath,
prefix =>
JsRaw("liftAjax.addPageName(" + S.encodeURL(prefix + "/" +
LiftRules.ajaxPath + "/").encJs + ")")) + ";\n" +
"if (FormData && s.data instanceof FormData) {\n" +
"\ts.processData = false;\n" +
"\ts.contentType = false;\n" +
"}\n" +
"jQuery.ajax(s);"
}

private def toJson(info: AjaxInfo, server: String, path: String =>
JsExp): String =
(("url : " + path(server).toJsCmd) ::
"data : " + info.data.toJsCmd ::
("type : " + info.action.encJs) ::
("dataType : " + info.dataType.encJs) ::
"timeout : " + info.timeout ::
"cache : " + info.cache :: Nil) ++
info.successFunc.map("success : " + _).toList ++
info.failFunc.map("error : " + _).toList mkString ("{ ", ", ", " }")
}
========= End MyJSArtifacts =========

2. Set the active JSArtifacts on LiftRules

LiftRules.jsArtifacts = MyJSArtifacts

3. Add the following JavaScript code, "uploadDocument" function is used
on the button that will open the file selection window (it small so it
could be added directly to the button on click instead of being a
separate function, style preferences). "uploadChanged" function is used
on a hidden input type="file" element onchange event

========= Start ajaxupload.js =========
function uploadDocument(fileId) {
$("#" + fileId).click();
},

function uploadFormData(fileId, funcName) {
var data = new FormData();
data.append(funcName, document.getElementById(fileId).files[0]);
return data;
}
========= End ajaxupload.js =========

4. I am using a Field instance, but this code can be adapted to work
without using fields. The HTML is a hidden file upload element and a
visible button, when the button in pressed the we call click() on the
file upload element, when the user select the files, the onchange event
handler make the Ajax call using the FormData object. Note: SimpleField
and AutoIdentifierField are utility traits the we use for simple fields
like this one

========= Start FilesField.scala =========
class FilesField
extends MandatoryTypedField[List[FileParamHolder]]
with SimpleField[List[FileParamHolder]]
with AutoIdentifierField {
def defaultValue = Nil

override def toForm: Box[NodeSeq] = {
val baseId = uniqueFieldId.open_!
val fileId = baseId + "_file";
val onchange = fileUpload(fileId, f => set(f :: get))
val onclick = Call("uploadDocument", fileId)

Full(
List(
<input id={ fileId } style="display:none" type="file"
onchange={ onchange.toJsCmd }/>,
<button id={ baseId } class="btn" type="button" tabindex={
tabIndex.toString } onclick={ onclick.toJsCmd }>
Upload
</button>)) // TODO NLS
}

private def fileUpload(fileId: String, func: FileParamHolder => Any):
JsExp = {
val f2: FileParamHolder => Any = fp => if (fp.length > 0) func(fp)
fmapFunc(BinFuncHolder(f2)) { name =>
val data = Call("uploadFormData", fileId, name);
SHtml.makeAjaxCall(data)
}
}
}

trait SimpleField[A] {
def setFromString(s: String): Box[A] = throw new
UnsupportedOperationException

def setFromAny(in: Any): Box[A] = throw new UnsupportedOperationException

def setFromJValue(jvalue: JValue): Box[A] = throw new
UnsupportedOperationException

def asJs: JsExp = throw new UnsupportedOperationException

def asJValue: JValue = throw new UnsupportedOperationException
}

trait AutoIdentifierField extends FieldIdentifier {
override lazy val uniqueFieldId: Box[String] = Full(Helpers.nextFuncName)
}
========= End FilesField.scala =========

Any recommendations on how to catch the file size exceeded exception? I
don't want to increase it too much only be able to return JavaScript to
the browser on error. I don't see a way to capture it because parameters
are processed before the server side handler function is called.
Validation could be done client side with the File API but as always the
server should be able to file gracefully if for some reason the browser
or someone send a fake request

Diego Medina

unread,
Feb 8, 2013, 5:33:36 PM2/8/13
to Lift

Nice!, it would be great if you could add it to the wiki. (not sure about your question about size)

Diego
Sent from my android cell

--
--
Lift, the simply functional web framework: http://liftweb.net
Code: http://github.com/lift
Discussion: http://groups.google.com/group/liftweb
Stuck? Help us help you: https://www.assembla.com/wiki/show/liftweb/Posting_example_code

--- You received this message because you are subscribed to the Google Groups "Lift" group.
To unsubscribe from this group and stop receiving emails from it, send an email to liftweb+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.


Jeppe Nejsum Madsen

unread,
Feb 11, 2013, 4:23:22 AM2/11/13
to lif...@googlegroups.com
Robert Marcano <rob...@marcanoonline.com> writes:


[...]

> Any recommendations on how to catch the file size exceeded exception?
> I don't want to increase it too much only be able to return JavaScript
> to the browser on error. I don't see a way to capture it because
> parameters are processed before the server side handler function is
> called. Validation could be done client side with the File API but as
> always the server should be able to file gracefully if for some reason
> the browser or someone send a fake request

We've solved this (using the "old" iframe approach) on the client side
by snooping on the HTTP Status of the upload request. You'll get a 500
on the file size exceeded exception.

Alternatively, you can do something in your Lift error handler.

/Jeppe

Robert Marcano

unread,
Feb 20, 2013, 2:28:36 PM2/20/13
to lif...@googlegroups.com
On 02/08/2013 06:03 PM, Diego Medina wrote:
> Nice!, it would be great if you could add it to the wiki. (not sure
> about your question about size)
>

This is being updated to run outside the lift ajax code, will update on
the list and later and the wiki when ready. The current code only works
if the upload is fast enough to not reach the time limit for ajax
requests. I am building the ajax_request the same way Lift does but
calling jQuery.ajax directly.

I think there should be a way to create ajax request with and official
API that runs outside the in browser lift ajax queue

> Diego
> Sent from my android cell
>
> On Feb 8, 2013 2:10 PM, "Robert Marcano" <rob...@marcanoonline.com
> <mailto:rob...@marcanoonline.com>> wrote:
>
> I implemented a file upload field that use the modern FormData and
> File API
>
> There is already a documented method to do this using iframes at
> https://www.assembla.com/__spaces/liftweb/wiki/AJAX_File___Upload
> document.getElementById(__fileId).files[0]);
> return data;
> }
> ========= End ajaxupload.js =========
>
> 4. I am using a Field instance, but this code can be adapted to work
> without using fields. The HTML is a hidden file upload element and a
> visible button, when the button in pressed the we call click() on
> the file upload element, when the user select the files, the
> onchange event handler make the Ajax call using the FormData object.
> Note: SimpleField and AutoIdentifierField are utility traits the we
> use for simple fields like this one
>
> ========= Start FilesField.scala =========
> class FilesField
> extends MandatoryTypedField[List[__FileParamHolder]]
> with SimpleField[List[__FileParamHolder]]

Channing Walton

unread,
Feb 10, 2014, 6:01:39 PM2/10/14
to lif...@googlegroups.com
Hi all. I'm curious about what happened with this?
Reply all
Reply to author
Forward
0 new messages