otro - AngularJS: carga archivos usando $ recurso(solución)
ng title (3)
Tenga en cuenta que este método no funcionará en 1.4.0+. Para obtener más información, consulte el registro de cambios de AngularJS (busque
$http: due to 5da1256
) y este problema . Esto fue en realidad un comportamiento involuntario (y por lo tanto eliminado) en AngularJS.
Se me ocurrió esta funcionalidad para convertir (o anexar) datos de formulario en un objeto FormData. Probablemente podría usarse como un servicio.
La siguiente lógica debe estar dentro de una configuración transformRequest
, o dentro de $httpProvider
, o podría usarse como un servicio. En cualquier caso, el encabezado Content-Type
debe establecerse en NULL, y hacerlo difiere según el contexto en el que coloque esta lógica. Por ejemplo, dentro de una opción transformRequest
al configurar un recurso, usted hace:
var headers = headersGetter();
headers[''Content-Type''] = undefined;
o si configura $httpProvider
, puede usar el método indicado en la respuesta anterior.
En el siguiente ejemplo, la lógica se coloca dentro de un método transformRequest
para un recurso.
appServices.factory(''SomeResource'', [''$resource'', function($resource) {
return $resource(''some_resource/:id'', null, {
''save'': {
method: ''POST'',
transformRequest: function(data, headersGetter) {
// Here we set the Content-Type header to null.
var headers = headersGetter();
headers[''Content-Type''] = undefined;
// And here begins the logic which could be used somewhere else
// as noted above.
if (data == undefined) {
return data;
}
var fd = new FormData();
var createKey = function(_keys_, currentKey) {
var keys = angular.copy(_keys_);
keys.push(currentKey);
formKey = keys.shift()
if (keys.length) {
formKey += "[" + keys.join("][") + "]"
}
return formKey;
}
var addToFd = function(object, keys) {
angular.forEach(object, function(value, key) {
var formKey = createKey(keys, key);
if (value instanceof File) {
fd.append(formKey, value);
} else if (value instanceof FileList) {
if (value.length == 1) {
fd.append(formKey, value[0]);
} else {
angular.forEach(value, function(file, index) {
fd.append(formKey + ''['' + index + '']'', file);
});
}
} else if (value && (typeof value == ''object'' || typeof value == ''array'')) {
var _keys = angular.copy(keys);
_keys.push(key)
addToFd(value, _keys);
} else {
fd.append(formKey, value);
}
});
}
addToFd(data, []);
return fd;
}
}
})
}]);
Entonces con esto, puedes hacer lo siguiente sin problemas:
var data = {
foo: "Bar",
foobar: {
baz: true
},
fooFile: someFile // instance of File or FileList
}
SomeResource.save(data);
Estoy usando AngularJS para interactuar con un RESTful
web RESTful
, usando $resource
para abstraer las diversas entidades expuestas. Algunas de estas entidades son imágenes, por lo que necesito poder usar la acción save
de $resource
"object" para enviar datos binarios y campos de texto dentro de la misma solicitud.
¿Cómo puedo usar el servicio $resource
AngularJS para enviar datos y subir imágenes a un servicio web tranquilo en una sola solicitud POST
?
He buscado por todas partes y, aunque podría haberlo perdido, no pude encontrar una solución para este problema: cargar archivos usando una acción $ recurso.
Hagamos este ejemplo: nuestro servicio RESTful nos permite acceder a las imágenes haciendo solicitudes a /images/
endpoint. Cada Image
tiene un título, una descripción y la ruta que apunta al archivo de imagen. Usando el servicio RESTful, podemos obtener todos ellos ( GET /images/
), uno solo ( GET /images/1
) o agregar uno ( POST /images
). Angular nos permite usar el servicio de $ recursos para realizar esta tarea fácilmente, pero no permite la carga de archivos (que se requiere para la tercera acción) de manera inmediata (y no parecen estar planeando respaldarla en cualquier momento pronto ). ¿Cómo, entonces, podríamos usar el muy útil servicio de $ recursos si no puede manejar las cargas de archivos? ¡Resulta que es bastante fácil!
Vamos a utilizar el enlace de datos, ya que es una de las características increíbles de AngularJS. Tenemos el siguiente formulario HTML:
<form class="form" name="form" novalidate ng-submit="submit()">
<div class="form-group">
<input class="form-control" ng-model="newImage.title" placeholder="Title" required>
</div>
<div class="form-group">
<input class="form-control" ng-model="newImage.description" placeholder="Description">
</div>
<div class="form-group">
<input type="file" files-model="newImage.image" required >
</div>
<div class="form-group clearfix">
<button class="btn btn-success pull-right" type="submit" ng-disabled="form.$invalid">Save</button>
</div>
</form>
Como puede ver, hay dos campos de input
texto que están enlazados cada uno a una propiedad de un solo objeto, que he llamado newImage
. La input
archivo también está vinculada a una propiedad del newImage
objeto newImage
, pero esta vez he usado una directiva personalizada tomada directamente desde here . Esta directiva hace que cada vez que el contenido de la input
archivo cambie, un objeto FileList se ponga dentro de la propiedad binded en lugar de un fakepath
(que sería el comportamiento estándar de Angular).
Nuestro código de controlador es el siguiente:
angular.module(''clientApp'')
.controller(''MainCtrl'', function ($scope, $resource) {
var Image = $resource(''http://localhost:3000/images/:id'', {id: "@_id"});
Image.get(function(result) {
if (result.status != ''OK'')
throw result.status;
$scope.images = result.data;
})
$scope.newImage = {};
$scope.submit = function() {
Image.save($scope.newImage, function(result) {
if (result.status != ''OK'')
throw result.status;
$scope.images.push(result.data);
});
}
});
(En este caso estoy ejecutando un servidor NodeJS en mi máquina local en el puerto 3000, y la respuesta es un objeto json que contiene un campo de status
y un campo de data
opcional).
Para que la carga del archivo funcione, solo necesitamos configurar correctamente el servicio $ http, por ejemplo, dentro de la llamada .config en el objeto de la aplicación. Específicamente, necesitamos transformar los datos de cada solicitud de publicación a un objeto FormData, para que se envíe al servidor en el formato correcto:
angular.module(''clientApp'', [
''ngCookies'',
''ngResource'',
''ngSanitize'',
''ngRoute''
])
.config(function ($httpProvider) {
$httpProvider.defaults.transformRequest = function(data) {
if (data === undefined)
return data;
var fd = new FormData();
angular.forEach(data, function(value, key) {
if (value instanceof FileList) {
if (value.length == 1) {
fd.append(key, value[0]);
} else {
angular.forEach(value, function(file, index) {
fd.append(key + ''_'' + index, file);
});
}
} else {
fd.append(key, value);
}
});
return fd;
}
$httpProvider.defaults.headers.post[''Content-Type''] = undefined;
});
El encabezado Content-Type
se establece en undefined
porque establecerlo manualmente en multipart/form-data
no establecería el valor límite, y el servidor no podría analizar la solicitud correctamente.
Eso es. Ahora puede usar $resource
para save()
objetos que contienen tanto campos de datos estándar como archivos.
ADVERTENCIA Esto tiene algunas limitaciones:
- No funciona en navegadores más antiguos. Lo siento :(
Si su modelo tiene documentos "incrustados", como
{ title: "A title", attributes: { fancy: true, colored: false, nsfw: true }, image: null }
entonces necesita refactorizar la función transformRequest en consecuencia. Podría, por ejemplo,
JSON.stringify
los objetos anidados, siempre que pueda analizarlos en el otro extremoEl inglés no es mi idioma principal, así que si mi explicación es oscura dígame y trataré de reformularla :)
- Esto es solo un ejemplo. Puede ampliar esto dependiendo de lo que su aplicación necesite hacer.
Espero que esto ayude, ¡salud!
EDITAR:
Como señaló @david , una solución menos invasiva sería definir este comportamiento solo para aquellos $resource
que realmente lo necesitan, y no para transformar todas y cada una de las solicitudes hechas por AngularJS. Puedes hacer eso creando tu $resource
como este:
$resource(''http://localhost:3000/images/:id'', {id: "@_id"}, {
save: {
method: ''POST'',
transformRequest: ''<THE TRANSFORMATION METHOD DEFINED ABOVE>'',
headers: ''<SEE BELOW>''
}
});
En cuanto al encabezado, debe crear uno que satisfaga sus requisitos. Lo único que debe especificar es la propiedad ''Content-Type''
al establecerlo como undefined
.
La solución más mínima y menos invasiva para enviar $resource
solicitudes de $resource
con FormData
descubrí que es esta:
angular.module(''app'', [
''ngResource''
])
.factory(''Post'', function ($resource) {
return $resource(''api/post/:id'', { id: "@id" }, {
create: {
method: "POST",
transformRequest: angular.identity,
headers: { ''Content-Type'': undefined }
}
});
})
.controller(''PostCtrl'', function (Post) {
var self = this;
this.createPost = function (data) {
var fd = new FormData();
for (var key in data) {
fd.append(key, data[key]);
}
Post.create({}, fd).$promise.then(function (res) {
self.newPost = res;
}).catch(function (err) {
self.newPostError = true;
throw err;
});
};
});