Crear una UI personalizada de subida de archivos con JavaScript
Los campos de archivo predeterminados de HTML ofrecen funcionalidad básica, pero a menudo carecen de la flexibilidad y la facilidad de uso que requieren las aplicaciones web modernas. En este DevTip construiremos una UI personalizada de subida de archivos con JavaScript que mejora tanto la apariencia como la funcionalidad del campo estándar del navegador. Esta solución ofrece una experiencia de usuario más atractiva mediante subidas de múltiples archivos, vistas previas de archivos, validación robusta que incluye comprobaciones de la firma de los archivos y seguimiento del progreso durante las subidas.
Introducción a las UI personalizadas de subida de archivos
Crear una UI personalizada de subida de archivos te permite superar las limitaciones del campo de archivo predeterminado. Al integrar un cargador de archivos en el diseño de tu aplicación, puedes gestionar de forma eficiente subidas de múltiples archivos y ofrecer retroalimentación visual inmediata con vistas previas de archivos y mensajes de validación adaptados.
Limitaciones de los elementos de entrada de archivo predeterminados
El elemento <input type="file"> estándar presenta varios desafíos:
- Estilos inconsistentes entre navegadores.
- Control limitado sobre la apariencia.
- Falta de soporte para funciones avanzadas como la selección de múltiples archivos, las vistas previas o los mensajes de validación personalizados.
Configurar la estructura HTML
Comienza creando una estructura HTML básica para nuestro cargador de archivos personalizado:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="csrf-token" content="your-csrf-token-here" />
<title>Custom File Upload UI</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="file-upload" id="drop-zone">
<input type="file" id="file-input" multiple accept="image/jpeg,image/png,image/gif" />
<label for="file-input">Choose Files or Drag & Drop</label>
<div id="file-preview"></div>
<div id="error-messages"></div>
<div id="upload-progress"></div>
</div>
<button id="upload-button">Upload Files</button>
<script src="script.js"></script>
</body>
</html>
Esta estructura incluye:
- Un elemento
<input>con restricciones de tipo de archivo. - Una zona de arrastrar y soltar para subir archivos.
- Contenedores para las vistas previas de archivos, los mensajes de error y el progreso de la subida.
- Una etiqueta meta para el token CSRF que mejora la seguridad.
Aplicar estilos al campo de archivo con CSS
Aplica estilos a nuestro componente personalizado de subida de archivos:
.file-upload {
position: relative;
display: inline-block;
padding: 20px;
border: 2px dashed #ccc;
border-radius: 8px;
transition: border-color 0.3s;
}
.file-upload.dragover {
border-color: #007bff;
background-color: rgba(0, 123, 255, 0.1);
}
#file-input {
position: absolute;
width: 1px;
height: 1px;
clip-path: inset(50%);
overflow: hidden;
}
#file-input:focus-visible + label {
outline: 2px solid currentColor;
outline-offset: 3px;
}
.file-upload label {
display: inline-block;
padding: 10px 20px;
background-color: #007bff;
color: #fff;
cursor: pointer;
border-radius: 5px;
transition: background-color 0.3s;
}
.file-upload label:hover {
background-color: #0056b3;
}
#file-preview {
margin-top: 20px;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
#file-preview img {
width: 100px;
height: 100px;
object-fit: cover;
border-radius: 4px;
}
#error-messages {
color: #dc3545;
margin-top: 10px;
}
#upload-progress {
margin-top: 10px;
height: 4px;
background-color: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
#upload-progress .progress-bar {
height: 100%;
background-color: #28a745;
transition: width 0.3s ease;
}
#upload-button {
margin-top: 20px;
padding: 10px 20px;
background-color: #28a745;
color: #fff;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
#upload-button:hover {
background-color: #218838;
}
Implementar JavaScript para ampliar la funcionalidad
Añade JavaScript para gestionar la selección de archivos, el renderizado de las vistas previas, la validación y el seguimiento del progreso de la subida.
const fileInput = document.getElementById('file-input')
const preview = document.getElementById('file-preview')
const uploadButton = document.getElementById('upload-button')
const dropZone = document.getElementById('drop-zone')
const errorMessages = document.getElementById('error-messages')
const uploadProgress = document.getElementById('upload-progress')
let selectedFiles = []
// Enable drag and drop functionality with visual feedback
dropZone.addEventListener('dragover', (e) => {
e.preventDefault()
dropZone.classList.add('dragover')
})
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('dragover')
})
dropZone.addEventListener('drop', async (e) => {
e.preventDefault()
dropZone.classList.remove('dragover')
await handleFiles(Array.from(e.dataTransfer.files))
})
fileInput.addEventListener('change', async (e) => {
await handleFiles(Array.from(e.target.files))
})
uploadButton.addEventListener('click', handleUpload)
async function handleFiles(files) {
preview.innerHTML = ''
errorMessages.innerHTML = ''
selectedFiles = []
for (const file of files) {
const isValid = await validateFile(file)
if (isValid) {
selectedFiles.push(file)
const reader = new FileReader()
reader.onload = (e) => createPreview(e.target.result, file.name)
reader.readAsDataURL(file)
}
}
}
async function validateFile(file) {
const validTypes = ['image/jpeg', 'image/png', 'image/gif']
const maxSize = 2 * 1024 * 1024 // 2 MB
if (!validTypes.includes(file.type)) {
showError(`${file.name} is not a supported file type`)
return false
}
if (file.size > maxSize) {
showError(`${file.name} exceeds the 2 MB size limit`)
return false
}
const isSignatureValid = await checkFileSignature(file)
if (!isSignatureValid) {
showError(`${file.name} failed file signature validation`)
}
return isSignatureValid
}
function checkFileSignature(file) {
return new Promise((resolve) => {
const reader = new FileReader()
reader.onerror = () => resolve(false)
reader.onload = (e) => {
const arr = new Uint8Array(e.target.result).subarray(0, 4)
if (arr.length < 4) return resolve(false)
let header = ''
for (let i = 0; i < arr.length; i++) {
header += arr[i].toString(16).padStart(2, '0')
}
const validSignatures = {
'89504e47': 'image/png',
'47494638': 'image/gif',
}
const detectedType = header.startsWith('ffd8ff') ? 'image/jpeg' : validSignatures[header]
resolve(detectedType === file.type)
}
reader.readAsArrayBuffer(file.slice(0, 4))
})
}
function createPreview(src, alt) {
const container = document.createElement('div')
container.className = 'preview-item'
const img = document.createElement('img')
img.src = src
img.alt = alt
const fileName = document.createElement('span')
fileName.textContent = alt
container.appendChild(img)
container.appendChild(fileName)
preview.appendChild(container)
}
function showError(message) {
const error = document.createElement('div')
error.textContent = message
errorMessages.appendChild(error)
}
function handleUpload() {
if (selectedFiles.length === 0) {
showError('Please select files to upload')
return
}
const formData = new FormData()
const csrfToken = document.querySelector('meta[name="csrf-token"]').content
selectedFiles.forEach((file) => {
formData.append('files[]', file)
})
formData.append('csrf_token', csrfToken)
const xhr = new XMLHttpRequest()
// Initialize progress tracking
uploadProgress.innerHTML = '<div class="progress-bar"></div>'
const progressBar = uploadProgress.querySelector('.progress-bar')
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100
progressBar.style.width = percentComplete + '%'
}
})
xhr.onload = function () {
if (xhr.status === 200) {
preview.innerHTML = ''
uploadProgress.innerHTML = ''
showSuccess('Files uploaded successfully!')
} else {
showError('Upload failed. Please try again.')
}
}
xhr.onerror = function () {
showError('An error occurred during upload')
}
xhr.open('POST', '/upload', true)
xhr.send(formData)
}
function showSuccess(message) {
const success = document.createElement('div')
success.style.color = '#28a745'
success.textContent = message
errorMessages.appendChild(success)
}
Esta implementación actualizada incorpora soporte para arrastrar y soltar, validación asíncrona robusta de archivos que incluye comprobaciones de la firma de los archivos y seguimiento visual del progreso durante las subidas. También garantiza retroalimentación para el usuario con mensajes de error y de éxito, respaldada por protección CSRF.
Estas breves comprobaciones de firma mejoran la retroalimentación, pero no permiten determinar que un archivo sea seguro ni que se pueda decodificar por completo. Valida el contenido y el token CSRF en el servidor antes de confiar en una subida.
Compatibilidad con navegadores
Usa un navegador moderno con FileReader, FormData, Drag and Drop, XMLHttpRequest y soporte para
async/await. Detecta la disponibilidad de las API y mantén una alternativa con el campo de archivo nativo cuando sea necesario.
Los polyfills por sí solos no pueden hacer que la sintaxis más reciente de JavaScript funcione en
navegadores antiguos.
Conclusión
Esta UI personalizada de subida de archivos demuestra un enfoque moderno para gestionar las subidas de archivos con mayor seguridad y mejor experiencia de usuario. Al integrar funciones como arrastrar y soltar, el seguimiento del progreso y una validación completa, puedes mejorar significativamente la funcionalidad del campo de archivo predeterminado.
Para una solución de nivel empresarial que ofrece funciones adicionales como subidas reanudables e integración con almacenamiento en la nube, considera usar Uppy 4.0. Uppy ofrece soporte para TypeScript e integraciones fluidas con frameworks populares como React, Vue, Svelte, Angular y Next.js.
