Merida Design Blog

Publicado el | Tutoriales / , ,

Como crear un componente de ReactJS para input file personalizado con vista previa y drag & drop

ReactJS es una librería creada y mantenida por el equipo de Facebook que te permite construir aplicaciones web basadas en componentes. Ya que ReactJS solo se encarga de la parte visual y el manejo interno del estado de los componentes, es posible usarlo en conjunto con otras librerías o frameworks como AngularJS o Meteor, sin embargo, eso es tema para otro artículo,

En este artículo, el número 5 en la serie del Input file personalizado usando distintas tecnologías, vamos a crear un componente de ReactJS que permite al igual que en los demás artículos de la serie, crear la interfaz para un campo de entrada de archivos para imágenes con vista previa y soporte para drag & drop.


El resultado final será igual al siguiente ejemplo.

En el demo se carga el sistema de módulos a través de SystemJS, pero no lo recomiendo para entorno de producción ya que no es posible minificar u ofuscar el código.


Configuración

Antes de comenzar a programar nuestro componente necesitamos configurar Webpack y Babel para hacer uso de los componentes como módulos y convertir el JSX de React a funciones javascript.

Webpack y Babel

Crea la carpeta para el proyecto, ubícate en ella en tu terminal y ejecuta el siguiente comando:

npm init

En la terminal te aparecerán varias preguntas para definir el proyecto, solo presiona enter hasta finalizar.
En tu carpeta ahora cuentas con el archivo del proyecto package.json que nos servirá para instalar los paquetes que necesitamos.

{
  "name": "react-file-uploader",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "..."
  },
  "author": "",
  "license": "ISC"
}

Modifica el archivo package.json y coloca el siguiente código.

{
    "name": "react-file-uploader",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "test": "..."
    },
    "author": "",
    "license": "ISC",
    "dependencies": {
        "babel-core": "latest",
        "babel-loader": "latest",
        "babel-preset-es2015": "latest",
        "babel-preset-react": "latest",
        "react": "latest",
        "react-dom": "latest",
        "webpack": "^latest"
    }
}

Las primeras 2 dependencias, babel-core y babel-loader, son las que necesitamos para poder usar babel, las siguientes 2 que contienen la palabra preset, nos permiten usar la sintaxis de ES2015 y JSX de React. Los últimos como te puedes imaginar, son la librería de ReactJS y webpack.

Una vez realizados los cambios, ejecuta el siguiente comando en tu terminal y espera a que finalice la instalación.

npm install

Webpack puede instalarse también de manera global usando el comando npm install -g webpack, sin embargo, en este ejemplo te voy a mostrar como usarlo de manera local para evitar posibles problemas de incompatibilidad de versiones y mantener todo lo requerido para el funcionamiento dentro del mismo paquete y así poder echarlo andar con solo clonar el proyecto e instalar las dependecias.

En el archivo package.json agrega los siguiente en la sección de scripts:

...
"scripts": {
    "compile": "./node_modules/webpack/bin/webpack.js",
    "compile-dev": "./node_modules/webpack/bin/webpack.js --watch",
    "compile-prod": "NODE_ENV=production ./node_modules/webpack/bin/webpack.js"
},
  • compile ejecuta webpack compilando el proyecto sin minificar el código.
  • compile-dev compila y mantiene a webpack observando cualquier cambio en los archivos del proyecto y recompilando de manera automática.
  • compile-prod compila y minifica el proyecto, listo para subir a producción.

Estas son las opciones que siempre incluyo en mis proyectos, pude que no necesites todas pero te recomiendo que incluyas al menos la versión de producción para optimizar tu aplicación.

La variable NODE_ENV es una variable de entorno de NodeJS a la que podemos acceder al momento de compilar y que veremos como usar a continuación.

Para usar webpack solo nos resta darle las instrucciones de como queremos que se construya el archivo final a través de un archivo de javascript.

Crea el archivo webpack.config.js en el directorio raiz de tu proyecto y coloca el siguiente código.

var debug = process.env.NODE_ENV !== "production";
var webpack = require('webpack');

module.exports = {
    context: __dirname,
    devtool: debug ? "inline-sourcemap" : null,
    entry: "./app/jsx/app.jsx",
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                exclude: /(node_modules|bower_components)/,
                loader: 'babel-loader',
                query: {
                    presets: ['react', 'es2015']
                }
            }
        ]
    },
    output: {
        path: "./public/js",
        filename: "bundle.js"
    },
    plugins: debug ? [] : [
        new webpack.optimize.DedupePlugin(),
        new webpack.optimize.OccurenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin({ mangle: false, sourcemap: false }),
    ],
    resolve: {
        extensions: ['', '.js', '.jsx']
    }
};

Veamos los puntos mas relevantes del código anterior.

En la primera linea hacemos uso de la variable NODE_ENV que comentamos anteriormente para asignar la variable debug que determina si estamos en modo de desarrollo.

La propiedad context define la ruta principal a partir de la cual se tomarán como relativas las rutas en otras propiedades.

En la propiedad entry indicamos el archivo principal a partir del cual se cargarán los otros módulos de la aplicación.

La propiedad loaders dentro de module nos sirve para indicar que babel será el modulo que usaremos para convertir el código, y por medio de los presets react y es2015 agregamos soporte para JSX que es el lenguaje usado por ReactJS para la estructura de los componetes, y también soporte para las últimas propiedades y características agregadas a javascript en la especificación del EcmaScript 2015.

En la propiedad output definimos la ruta y el nombre del archivo que será generado.

En los plugins indicamos que cuando no estemos compilando en entorno de desarrollo se ejecuten los plugins que optimizarán el código eliminando código duplicado y minificandolo.

Ahora ya podemos comenzar a desarrollar el componente y compilar el proyecto usando cualquiera de los siguientes comandos.

npm run compile
npm run compile-dev
npm run compile-prod

HTML

Con el entorno de trabajo listo es momento de comenzar a maquetar la aplicación. Crea una carpeta con el nombre public y dentro de ésta, el archivo index.html y coloca el siguiente código.

<!DOCTYPE html>
<html>

  <head>
    <title>Campo para imágenes personalizado</title>
    <link href = "https://file.myfontastic.com/SLzQsLcd7FmmzjBYTcyVW3/icons.css" rel="stylesheet">
    <link href = "fileuploader.css" rel = "stylesheet">
  </head>

  <body>
    <div id="app"></div>
  </body>

</html>

El elemento con id app será el que usaremos para montar el componente, pero antes de eso veamos los estilos.


Estilos

Siempre en la carpeta public, crea un archivo con el nombre fileuploader.css y coloca el siguiente código.

/* Presentation Styles */
html, body {
  height: 100%;
  margin: 0;
  padding: 0; 
}

body {
  align-items: center;
  background-color: #eee;
  display: flex;
  justify-content: center; 
}

button {
    display: block;
    margin: 2em auto;
}

/* File Uploader Styles  */

.uploader input {
  display: none; 
}

.uploader {
  align-items: center;
  background-color: rgba(0, 0, 0, 0.02);
  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.dragging {
  outline-color: orangered; 
}

.uploader.dragging .icon {
  color: orangered; 
}

.uploader.loaded .icon {
  color: #fff;
  color: rgba(255, 255, 255, 0.5); 
}

.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; 
}

Si no comprendes bien que es lo que hacen las propiedades definidas en la hoja de estilos, tal vez quieras leer también el primer artículo de la serie en donde explico lo que hacen de manera breve.

Nota: Estoy omitiendo todos los prefijos de los navegadores para mayor legibilidad en el artículo, pero es necesario colocarlos para tener soporte en todos los navegadores. El código con los prefijos lo puedes consultar en el demo.


Componentes

Primero vamos a crear el componente principal de la aplicación, que se encargará a su vez de cargar el componente para nuestro control personalizado.

Crea la carpeta app y dentro de ésta el archivo con el nombre App.jsx y coloca el siguiente código:

import React from "react";
import ReactDOM from "react-dom";
import FileUploader from "./FileUploader";

class App extends React.Component {
    constructor(props) {
        super(props);
        this.onClick = this.onClick.bind(this);
    }
    
    onClick(e) {
        e.preventDefault();
        
        var url = 'http://nowhere.com',
            fileObject = this.refs.uploader.getFileObject(),
            formData = new FormData(document.getElementById('form'));
            
        if (undefined === fileObject) return;
        
        formData.append('file', fileObject);
        fetch(url, {
            method: 'POST',
            body: formData
        });
    }
    
    render() {
        return (
            <div>
                <FileUploader ref="uploader" />
                <button onClick={this.onClick}>Enviar</button>
            </div>
        );
    }
}


ReactDOM.render(<App />, document.querySelector('#app'));

Primero leemos la librería principal de react y react-dom que se encarga de montar el componente en el DOM, así como el componente FileUploader que crearemos en breve.

En el método onClick accedemos al objeto de archivo almacenado en el componente de captura de imagen a través del método getFileObject del propio componente. React nos permite acceder a propiedades y métodos de los componentes a través de un valor de referencia asignado mediante el atributo ref.

Siempre en el mismo método, creamos un objeto formData que enviamos a través de una petición POST usando la api de fetch. Como te puedes imaginar, al presionar el botón no pasará nada porque la url no existe, pero en caso de existir enviaría correctamente la petición junto con el archivo.

Por último en el método render incluimos el componente FileUploader asignándole el atributo ref para acceder al componente, y el botón para hacer el envío post con la imagen.


Crea un archivo con el nombre FileUploader.jsx y coloca el siguiente código:

import React, { PropTypes } from "react";

const propTypes = {
    baseColor: PropTypes.string,
    activeColor: PropTypes.string,
    overlayColor: PropTypes.string
};

const defaultProps = {
    baseColor: 'gray',
    activeColor: 'green',
    overlayColor: 'rgba(255,255,255,0.3)'
};

class FileUploader extends React.Component {
    constructor(props) {
        super(props);
        
        this.state = {
            active: false,
            imageSrc: '',
            loaded: false
        }
        
        this.onDragEnter  = this.onDragEnter.bind(this);
        this.onDragLeave  = this.onDragLeave.bind(this);
        this.onDrop       = this.onDrop.bind(this);
        this.onFileChange = this.onFileChange.bind(this);
    }
    
    onDragEnter(e) {
        this.setState({ active: true });
    }
    
    onDragLeave(e) {
        this.setState({ active: false });
    }
    
    onDragOver(e) { 
        e.preventDefault(); 
    }
    
    onDrop(e) {
        e.preventDefault();
        this.setState({ active: false });
        this.onFileChange(e, e.dataTransfer.files[0]);
    }
    
    onFileChange(e, file) {
        var file = file || e.target.files[0],
            pattern = /image-*/,
            reader = new FileReader();
            
        if (!file.type.match(pattern)) {
            alert('Formato inválido');
            return;
        }
        
        this.setState({ loaded: false });
        
        reader.onload = (e) => {
            this.setState({ 
                imageSrc: reader.result, 
                loaded: true 
            }); 
        }
        
        reader.readAsDataURL(file);
    }
    
    getFileObject() {
        return this.refs.input.files[0];
    }
    
    getFileString() {
        return this.state.imageSrc;
    }
    
    render() {
        let state = this.state,
            props = this.props,
            labelClass  = `uploader ${state.loaded && 'loaded'}`,
            borderColor = state.active ? props.activeColor : props.baseColor,
            iconColor   = state.active 
                ? props.activeColor
                : (state.loaded) 
                    ? props.overlayColor 
                    : props.baseColor;
        
        return (
            <label 
                className={labelClass}
                onDragEnter={this.onDragEnter}
                onDragLeave={this.onDragLeave} 
                onDragOver={this.onDragOver}
                onDrop={this.onDrop}
                style={{outlineColor: borderColor}}>
                
                <img src={state.imageSrc} className={state.loaded && 'loaded'}/>
                <i className="icon icon-upload" 
                    style={{ color: iconColor }}></i>
                <input type="file" accept="image/*" onChange={this.onFileChange} ref="input" />
            </label>
        );
    }
}

FileUploader.propTypes = propTypes;
FileUploader.defaultProps = defaultProps;

export default FileUploader;

Al principio, definimos los tipos de valores y los valores por defecto que podrán asignarse a este componente, que servirán para cambiar los colores del borde y el ícono.

En el constructor definimos los valores iniciales de las propiedades de estado del componente que sirven para manipular la apariencia visual de la interfaz.

Las propiedades del estado del componente son:

  • active: Este valor se modifica cuando se arrastra por encima del componente o se abandona el mismo.
  • imageSrc: De aquí se toma el valor para mostrar en la vista previa.
  • loaded: Se modifica cuando la imagen se carga por completo.

El método onDragOver lo usamos solamente para cancelar el comportamiento por defecto del navegador.

Los métodos onDragEnter, y onDragLeave solamente servirán para cambiar la propiedad active del estado del componente.

El método onDrop que se ejecuta al soltar el archivo sobre el componente, lo usamos para ejecutar la función onFileChange pasándole el objeto del archivo arrastrado.

La función onFileChange también es la que se ejecuta cuando se detecta que el valor del input ha cambiado, es decir, cuando se ha seleccionado un archivo. Dentro de esta función creamos un objeto FileReader que se usa para leer el contenido del archivo que nos regresa como código base64 que a su vez asignamos a la propiedad imageSrc del state.

En el método render colocamos la estructura del componente asignando las funciones a los eventos que ya mencionamos.


Conclusión

Si deseas ver otras implementaciones de este ejemplo de campo personalizado con vista previa y drag & drop, te recomiendo visitar los otros artículos de la serie.

¿Te ha sido de utilidad este artículo?, ¿tienes alguna duda o sugerencia?, no olvides que puedes dejar tus observaciones en la sección de comentarios mas abajo.



Publicaciones que pueden interesarte

    Deja un comentario

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