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