Quizá alguno os hayáis preguntado cómo poder hacer un formulario con archivos adjuntos usando AngularJS (1.4), Spring, servicios REST, que envíe un mail de confirmación y devuelva datos en el response, y que funcione en internet explorer 9.
La respuesta corta es que no se puede.
A por la respuesta larga. ¡Yay!
Vamos por partes: Queríamos tener un input falso en el que se viera el nombre del archivo al adjuntarlo, maquetado a nuestro gusto (con los estilos para el input y el botón de subida que quisiéramos nosotros, no el navegador), y el input real no visible. Necesitábamos, además de “todo lo demás”, acceder al nombre del archivo para mostrarlo en el falso input. También teníamos una validación javascript que no permitía subir ficheros de más de 10 megas.
Para adjuntar archivos existe el módilo “angular-file-upload“, que es la primera que me recomendaron, pero no es compatible con las versiones 1.3 y 1.4 de Angular. Además, estas directivas se basan en la variable $file del navegador para recuperar el nombre del archivo que se quiere adjuntar, pero internet explorer, por “cuestiones de seguridad”, no deja acceder a la variable. Así que ni podemos tener su nombre ni comprobar su tamaño. Hay que buscar otra cosa.
Me bajé “ng-file-upload“, que tiene un .js diferente (FileApi) para IE8 e IE9, que utiliza flash para subirlos . Leed bien la documentación porque además de necesitar tener instalado flash en el navegador, aparte de adjuntar los .js pertinentes debéis añadir un trozo de código (la inicialización de FileApi) si queréis que funcione en esos navegadores.
El código del input en el html es este:
(no, no es una errata, ngf-select es el nombre de la directiva en ng-file-upload)
Siendo el primer input el oculto donde se subirá el archivo, y el segundo el falso input donde se mostrará el nombre. La imagen es el “icono” de selección de archivo. El input “abarca” al falso input y a la imagen, así cuando se haga click sobre alguno de ellos tendrá el mismo efecto que si se hace click sobre un input normal (porque es lo que se ha hecho).
La instrucción “onFileSelect” se define en el controlador:
$scope.onFileSelect = function($files) { //$files: an array of files selected, each file has name, size, and type. var $file = $files[0]; $scope.adjunto = $file; }
Se ve que $scope.adjunto se rellena con el fichero, y se puede acceder a adjunto.name para pintarlo en pantalla en el falso input. Esto es lo que hace en navegadores que no son IE9. Sinceramente, no me interesa saber qué hace en ese navegdor, pero funciona.
…
Para la llamada al servidor vamos a meter los datos del formulario en un formData:
var fd = new FormData(); fd.append('file', $scope.adjunto); var formulario = { "id" : "", "nombreApellidos" : "", "tipoId" : "", "valorId" : "", "email" : "", "telefono" : "", }; formulario.nombreApellidos = $scope.nombreApellidos; formulario.tipoId = $scope.tipoId; formulario.valorId = $scope.valorId; formulario.email = $scope.email; formulario.telefono = $scope.telefono; fd.append('formulario', angular.toJson(formulario));
Este código va en el controlador, antes de la llamada al servidor, que – no nos olvidemos que estamos usando Angular – es la siguiente:
$http.post(uploadUrl, fd, { transformRequest: angular.identity, headers: {'Content-Type': undefined} }) .success(function(data) { //acciones a realizar en caso de exito }).error(function(response) { //acciones a realizar en caso de error });
Y en el controlador del servidor:
@RequestMapping(value= uploadUrl, method=RequestMethod.POST) public @ResponseBody HashMap guardarFormulario(@RequestParam(value = "formulario", required = false) String store, @RequestPart("file") MultipartFile file){ try { AvisoVO avisoVO = new ObjectMapper().readValue(store, AvisoVO.class); AdjuntoVO archivoAdjunto = new AdjuntoVO(); archivoAdjunto.setNombreArchivo(file.getOriginalFilename()); archivoAdjunto.setContenido(file.getBytes()); archivoAdjunto.setTamanio(file.getSize()); avisoVO.setAdjunto(archivoAdjunto);
Esto sirve para navegadores compatibles con HTML5, pero IE9 no lo es. Y aquí se acabó la cuestión. Para hacer la subida, se debe añadir un condicional al código del .js que haga una simple llamada jQuery al servidor, y en el controlador habrá que añadir otro procedimiento para recibir y procesar la llamada:
if(isIE()){ $http({ url:uploadUrlIE, method: 'POST', data:fd, headers: {'Content-Type': undefined}, transformRequest: angular.identity }) .success(function(data, status, headers, config) { //acciones a realizar en caso de exito }).error(function(data, status, statusText, headers, config) { //acciones a realizar en caso de error }); }
(La función isIE() se puede rellenar fácilmente, si tenéis alguna duda le podéis echar un vistazo a esta otra entrada del blog)
y en el .java:
@RequestMapping(value=uploadUrlIE, method = RequestMethod.POST) public @ResponseBody String upload(MultipartHttpServletRequest request) { //get the files from the request object Iterator itr = request.getFileNames(); MultipartFile file = request.getFile(itr.next()); log.info(file.getOriginalFilename() +" uploaded!"); try{ AvisoVO avisoVO = new ObjectMapper().readValue(request.getParameter("formulario"), AvisoVO.class);
Esto también es así porque Internet Explorer 9 explota si le devuelves algo que no sea una cadena de texto en el response. Y queremos que devuelva algo. Queremos que devuelva una clave y un número de registro, resultado de haber hecho “cosas” con el formulario y haber mandado un email. Y en el procedimiento normal lo que devolvemos es un objeto. Pero en el procedimiento para IE9 tiene que ser un string. Así que vamos separando el código preparándonos para ello.
………………………………………
Esta entrada se me está haciendo muy larga y no sé vosotros, pero yo con entradas largas me mareo y al final no me entero de nada. Lo básico ya está hecho, de todas formas, solo me queda comentar el “problemilla” que tiene IE9 con el envío por correo electrónico y cómo montar el string para devolverlo desde el servidor y leerlo en el cliente (que es una cosa muy simplona que seguro que ya sabéis). Si tenéis un formulario y lo único que queréis es registrarlo y ya está, con esta entrada os debería funcionar.
Espero que esta entrada pueda ser de utilidad, y si no, como siempre, aquí tenéis un gato para compensar: