Cómo subir una imagen vía Ajax (usando Laravel como backend)

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.

Imagen 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).

Clic para modificar imagen de perfil

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).

Imagen de perfil modificada al instante

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 tipo file.
  • 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 valor photo (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 y title 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étodo serialize en este caso sólo captura el csrf 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, y path 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', '[email protected]');

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.

Parte 1. Explicación de los pasos a seguir.

Parte 2. Ejecución de lo propuesto.

ajax laravel

¿Tienes alguna duda?

Si algo no te quedó claro o tienes alguna sugerencia, escribe un comentario aquí debajo.

Además recuerda compartir el post si te resultó de ayuda. ¡Gracias!

Antes que te vayas

Inscríbete en nuestro curso gratuito de Laravel