Merida Design Blog

Publicado el | Tutoriales / , , , ,

Como crear directiva de AngularJS para input file personalizado con vista previa y drag & drop

Continuando con la serie sobre como crear un input file personalizado, en esta ocasión es el turno de AngularJS y aprender como convertir el plugin creado hasta el momento a una directiva.

En este artículo voy a tomar como base el plugin de jQuery que vimos en el artículo anterior ya que AngularJS cuenta con una versión lite de jQuery dentro de su core, por lo que podemos mantener la sintaxis.

El resultado final será el siguiente:

Recuerda que puedes copiar el código directamente de este demo


El HMTL

<!DOCTYPE html>
<html>
  <head>
      <title>Ng Uploader</title>
      <link href="https://file.myfontastic.com/SLzQsLcd7FmmzjBYTcyVW3/icons.css" rel="stylesheet">
      <link rel="stylesheet" href="file-uploader.css">
      <link rel="stylesheet" href="style.css">
  </head>

  <body ng-app="app" ng-controller="Controller as Ctrl">
      
      <form ng-submit="Ctrl.submit($event)">
          <file-uploader file="Ctrl.user.file" base-color="#ccc" active-color="green"></file-uploader>
          <button type="submit">Enviar</button>
      </form>
      
      
      <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.9/angular.min.js"></script>
      <script src="ng-file-uploader.js"></script>
      <script src="app.js"></script>
  </body>

</html>

La directiva será de tipo Elemento y remplazará la etiqueta file-uploader con el HMTL de nuestro plugin.

En la directiva definimos el modelo que guardará el contenido del archivo seleccionado para posteriormente enviarlo mediante AJAX a través del atributo file="Ctrl.user.file".


EL CSS

/* File Uploader Styles  */

.uploader input {
  display: none;
}

.uploader {
  align-items: center;
  background-color: #efefef;
  background-color: rgba(0, 0, 0, 0.02);
  cursor: pointer;
  display: -webkit-flex;
  display: flex;
  height: 300px;
  justify-content: center;
  outline: 3px dashed #ccc;
  outline-offset: 5px;
  position: relative;
  width: 300px;
}

.uploader img,
.uploader .icon {
  pointer-events: none;
}

.uploader,
.uploader .icon {
  transition: all 100ms ease-in;
}

.uploader .icon {
  color: #eee;
  color: rgba(0, 0, 0, 0.2);
  font-size: 5em;
}

.uploader img {
  left: 50%;
  opacity: 0;
  max-height: 100%;
  max-width: 100%;
  position: absolute;
  top: 50%;
  transition: all 300ms ease-in;
  transform: translate(-50%, -50%);
  z-index: -1;
}

.uploader img.loaded {
  opacity: 1;
}

El archivo de estilo se mantiene igual que la versión para el plugin de jQuery.


La Directiva

Para poder acceder a los elementos del DOM directamente desde AngularJS, las directivas cuentan con la función link, que se ejecuta inmediatamente después de que el DOM se terminó de leer, por lo que una vez que el HTML del file uploader ha sido insertado podemos acceder a todos sus componentes.

(function() {
    'use strict';

    // 1 Modulo
    angular.module('ngUploader', [])
        .directive('fileUploader', FileUploader);

    function FileUploader() {
        return {
            // 2 Tipo de directiva
            restrict: 'E',
            replace: true,
            // 3 Valores de configuración
            scope: {
                activeColor: '@',
                baseColor: '@',
                file: '='
            },
            
            // 4 Plantilla
            template: "" +
                "<label class='uploader' ondragover='return false'>" +
                    "<img />" +
                    "<i class='icon icon-upload'></i>" +
                    "<input type='file' accept='image/*' />" +
                "</label>",

            // 5 Funcionalidad
            link: function(scope, element, attrs) {
                var settings = angular.extend({
                    baseColor: '#ccc',
                    activeColor: 'green',
                    overlayColor: 'rgba(255,255,255,0.5)'
                }, scope);

                var $label = element,
                    $fileInput = $label.find('input'),
                    $icon = $label.find('i'),
                    $img = $label.find('img');
                    
                _setInactive();
                suscribe();

                function suscribe() {
                    $fileInput.on('change', _handleInputChange);
                    $img.on('load', _handleImageLoaded);
                    $label.on('dragenter', _handleDragEnter);
                    $label.on('dragleave', _handleDragLeave);
                    $label.on('drop', _handleDrop);
                }

                function _handleDragEnter(e) {
                    e.preventDefault();
                    _setActive();
                }

                function _handleDragLeave(e) {
                    e.preventDefault();
                    _setInactive();
                }

                function _handleDrop(e) {
                    e.preventDefault();
                    _setInactive();
                    $fileInput[0].files = e.dataTransfer.files;
                    _handleInputChange();
                }

                function _handleImageLoaded() {
                    if (!$img.hasClass('loaded')) {
                        $img.addClass('loaded');
                    }

                    _setInactive();
                }

                function _handleInputChange(e) {
                    var file = (undefined !== e) ? e.target.files[0] : $fileInput[0].files[0];

                    var pattern = /image-*/;
                    var reader = new FileReader();

                    if (!file.type.match(pattern)) {
                        alert('invalid format');
                        return;
                    }

                    if ($label.hasClass('loaded')) {
                        $label.removeClass('loaded');
                    }

                    reader.onload = _handleReaderLoaded;
                    reader.readAsDataURL(file);
                    
                    // 6 Modelo del archivo
                    scope.file = file;
                    scope.$apply();
                }

                function _handleReaderLoaded(e) {
                    var reader = e.target;
                    $img[0].src = reader.result;
                    $label.addClass('loaded');
                }

                function _setActive() {
                    $label.css('outline-color', settings.activeColor);
                    $icon.css('color', settings.activeColor);
                }

                function _setInactive() {
                    $label.css('outline-color', settings.baseColor);
                    $icon.css('color', ($img.hasClass('loaded') ? settings.overlayColor : settings.baseColor));
                }
            }
        }
    }

}());

1 Módulo: Creamos el módulo ngUploader para que la directiva pueda ser incluida como dependencia sin problema en cualquier módulo AngularJS.

2 Tipo de directiva: Le indicamos a Angular que la directiva será de tipo elemento (línea 13) y que debe ser reemplazada por el contenido de la plantilla (punto 4), esto nos permite usar la etiqueta file-uploader.

3 Valores de configuración: Asignamos al scope de la directiva, los valores que se asignaron como atributos y que nos sirven para determinar la apariencia del plugin.

La @ indica que el valor se tomará como un string y el = toma el valor tal cual es, en este caso, un objeto de javascript.

4 Plantilla: Aquí se define el contenido que será colocado en lugar de la etiqueta, no olvides que es necesario incluir el atributo ondragover="return false", de lo contrario el drag & drop no funciona.

5 Funcionalidad: La función link recibe el scope de la directiva, al cual ya hemos asignado los valores de configuración y element, que es el contenedor principal de la plantilla HMTL envuelto en un selector de jQuery lite ([label]).

6 Modelo del archivo: Cuando el valor del input file cambia, asignamos el valor del objeto File al modelo que fue especificado en el scope para que pueda ser manipulado fuera de la directiva.

La explicación de las funciones internas la puedes encontrar en el primer artículo; “Como crear un input file personalizado con vista previa y drag & drop”.


Módulo y controlador de la aplicación

(function() {
    'use strict';

    angular.module('app', ['ngUploader'])
        .controller('Controller', Controller);

    Controller.$inject = ['$http'];

    function Controller($http) {
        var vm = this;

        vm.user = { file: null };

        vm.submit = function($event) {
            $event.preventDefault();

            $http.post('http://nowhere.com', vm.user, {
                headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
                transformRequest: function(data) {
                    if (undefined === data) return data;

                    var formData = new FormData();

                    angular.forEach(data, function(value, key) {
                        formData.append(key, value);
                    });
                    
                    return formData;
                }
            });
        }
    }
}());

Para que la aplicación reconozca la directiva, solamente debes incluir el modulo ngUploader a las dependencias de tu módulo principal y listo, automáticamente verás como se convierte en el control personalizado como el del demo.


Puntos a destacar

En la línea 12 declaramos los modelos para enviar en el formulario, en este demo solo he incluido el del archivo pero si necesitas incluir otros como nombre, correo, etc., los puedes agregar directo como propiedades del vm.user y colocarlos como ng-model en su correspondiente campo dentro del formulario.

Para que los archivos sean incluidos en la petición AJAX, es necesario transformar el objeto vm.user a un FormData y especificar en las cabeceras que será un objeto de formulario que se enviará y no un json.

Si no te funciona el ejemplo del AJAX, prueba cambiando el Content-Type de la cabecera a undefined, de esa forma al enviarse la petición, el tipo de contenido se determina automáticamente.


Conclusión

En el siguiente tutorial de la serie vamos a ver como crear un componente usando Angular 2 y TypeScript, no olvides suscribirte en el formulario de abajo para estar al tanto de las nuevas publicaciones.



Publicaciones que pueden interesarte

    Deja un comentario

      tope
    Derechos Reservados, Merida Design 2017
    %d bloggers like this: