Cómo subir una imagen vía Ajax (usando Laravel como backend)
Tiempo de lectura: 4.03 minutos
Qué vamos a hacer
Lo que vamos a hacer, respecto a cómo lo ve el usuario de la aplicación, es lo siguiente.
El usuario tiene una imagen de perfil por defecto.
El usuario hace clic sobre su imagen de perfil, y de pronto puede seleccionar una nueva imagen (aunque también puede presionar cancelar y no pasa nada).
Si el usuario selecciona una imagen y acepta, entonces su imagen de perfil se actualiza inmediatamente (sin necesidad de recargar la página los cambios se mantienen).
Cómo lo vamos a hacer
Tenemos muchas formas de implementar esta característica.
En este caso lo haremos de la siguiente manera:
- Vamos a añadir un formulario (pero con estilos haremos que permanezca oculto). Este formulario va a contener un campo de tipo
file
. - Vamos a usar Javascript para asociar un evento de
click
sobre la imagen de perfil. Cuando se detecte este evento, vamos a provocar un click sobre el input de tipofile
. - Vamos a escuchar el evento
change
del input, para que cuando se haya escogido un archivo, realicemos una petición AJAX para modificar la imagen de perfil. - La petición AJAX devolverá un mensaje de éxito o fallo. Si la respuesta fue exitosa, entonces vamos a actualizar la imagen de perfil vía Javascript (para no actualizar la página).
Hagámoslo
El formulario oculto estará escondido gracias a una propiedad de CSS llamada display
(con el valor none
).
En este caso, lo estoy ubicando justo antes de la imagen.
<form action="{{ url('perfil/foto') }}" method="post" style="display: none" id="avatarForm">
{{ csrf_field() }}
<input type="file" id="avatarInput" name="photo">
</form>
<img src="{{ auth()->user()->getAvatarUrl() }}" id="avatarImage">
Nótese que:
- El formulario, al igual que la imagen, y el input, tienen un id asignado (esto es importante para acceder a estos elementos vía Javascript).
- El input de tipo
file
tiene un atributo name con el valorphoto
(este valor debe coincidir con lo que tengamos en nuestro controlador, para recuperar el archivo subido correctamente desde backend). - He simplificado el código quitando el atributo
class
,alt
ytitle
a mi imagen (ustedes pueden usar estos atributos según les convenga en sus proyectos).
¿Qué hacemos con Javascript?
Como ya lo he comentado antes, necesitamos registrar 2 eventos. Y así mismo obtener una referencia de los elementos.
$(function () {
var $avatarImage, $avatarInput, $avatarForm;
$avatarImage = $('#avatarImage');
$avatarInput = $('#avatarInput');
$avatarForm = $('#avatarForm');
$avatarImage.on('click', function () {
$avatarInput.click();
});
$avatarInput.on('change', function () {
alert('change');
});
});
Como habrás notado, en el evento change
del input sólo he puesto un alerta.
Hasta este punto debes asegurarte de obtener ese alerta, luego de hacer clic en la imagen y seleccionar un archivo.
Si todo está conforme, entonces ya puedes reemplazar el alert
para realizar la petición Ajax.
Así tendríamos lo siguiente:
$avatarInput.on('change', function () {
var formData = new FormData();
formData.append('photo', $avatarInput[0].files[0]);
$.ajax({
url: $avatarForm.attr('action') + '?' + $avatarForm.serialize(),
method: $avatarForm.attr('method'),
data: formData,
processData: false,
contentType: false
}).done(function (data) {
if (data.success)
$avatarImage.attr('src', data.path);
}).fail(function () {
alert('La imagen subida no tiene un formato correcto');
});
});
Aquí debemos tener en cuenta que:
- Estamos obteniendo la url a la que se hará la petición, y el
method
a partir de los valores definidos en el formulario. - Usamos un objeto
FormData
para subir la imagen vía Ajax (ya que el métodoserialize
en este caso sólo captura elcsrf token
). - La respuesta que obtenemos de la petición Ajax se compone de un objeto al que llamamos
data
y que tiene 2 atributos (success
para indicar si la operación tuvo éxito, ypath
con la ruta hacia la imagen de perfil).
Por último, sólo nos hace falta tener registrada la ruta perfil/foto
(que fue la que usamos en el action
del formulario). Si lo prefieres, puedes usar una ruta distinta.
Esta ruta debe declararse en el archivo de rutas de Laravel.
Route::post('/perfil/foto', 'ProfileController@updatePhoto');
En este caso, la ruta se resuelve a través de un controlador llamado ProfileController
. Específicamente a través de su método updatePhoto
.
Es así que tendríamos lo siguiente en dicho controlador:
public function updatePhoto(Request $request)
{
$this->validate($request, [
'photo' => 'required|image'
]);
$file = $request->file('photo');
$extension = $file->getClientOriginalExtension();
$fileName = auth()->id() . '.' . $extension;
$path = public_path('images/users/'.$fileName);
Image::make($file)->fit(144, 144)->save($path);
$user = auth()->user();
$user->photo_extension = $extension;
$saved = $user->save();
$data['success'] = $saved;
$data['path'] = $user->getAvatarUrl() . '?' . uniqid();
return $data;
}
Este método:
- Valida que el campo
photo
enviado en la petición sea una imagen (y es un además un campo obligatorio). - Obtiene la extensión de la imagen, de tal forma que el id del usuario, seguido de la extensión sea el nombre del archivo a guardar.
- Hace uso del facade
Image
para que si la imagen es mayor a 144x144 píxeles, entonces se ajuste su tamaño en función a este límite. - Guarda en la tabla de usuarios, en la columna
photo_extension
, la extensión de la imagen subida. - Finalmente devuelve una respuesta en JSON indicando si la operación fue exitosa, y además envía de vuelta la URL de la imagen.
Para que el código anterior funcione correctamente, es necesario que en la tabla de usuarios tengas una columna photo_extension
.
$table->string('photo_extension')->nullable();
También es necesario que definas el método getAvatarUrl
en el modelo User
.
public function getAvatarUrl()
{
if ($this->photo_extension)
return asset('images/users/'.$this->id.'.'.$this->photo_extension);
return asset('images/users/default.jpg');
}
Este método devuelve una URL absoluta hacia la imagen de perfil del usuario. Y en caso de no existir, la URL absoluta de una imagen por defecto.
Si no has instalado aún el paquete Intervention/Image (necesario para que funcione la redimensión de imágenes), lo puedes instalar simplemente ejecutando:
composer require intervention/image
Por cierto. Si eres observador habrás notado el uso de uniqid()
en el método updatePhoto
. Éste método se usa para añadir un número único al final del nombre del archivo, y asegurar de esta forma, que el usuario vea siempre su nueva imagen de perfil (ya que es posible que anteriormente haya subido una imagen con la misma extensión, y que esta imagen se haya guardado en la memoria caché del navegador).
Videotutorial
Este artículo lo he escrito en conmemoración a un par de videos que grabé ya hace un buen tiempo sobre el mismo tema.
Si prefieres, puedes seguir el tutorial en formato de video. Aunque, debes tener en cuenta que hay ligeras diferencias respecto al video y este tutorial escrito.