Quando si vuole offrire all’utente la possibilità di caricare più file di grandi dimensioni in maniera semplice, è comodo utilizzare un’area di drag & drop personalizzata e un dialog che copra l’intera schermata, così da indicare chiaramente lo stato del caricamento. In Vaadin 24 esiste il componente Upload, che può essere nascosto e controllato manualmente in JavaScript, sfruttando al tempo stesso i metodi server-side di Vaadin per gestire correttamente la ricezione dei file.
In questo esempio si parte da un layout che occupa l’intero schermo, impostato con setSizeFull. Dentro la vista, si istanzia un componente Upload collegato a MultiFileMemoryBuffer, ma gli si impone uno stile display: none. Grazie a questa impostazione, Upload diventa invisibile e non interferisce con l’interfaccia personalizzata. Per permettere all’utente di trascinare i file, si crea un VerticalLayout con uno stile che gli conferisce un bordo tratteggiato, degli angoli arrotondati e del padding per renderlo gradevole. Il layout funge così da contenitore visibile su cui trascinare i file.
All’interno di questo contenitore viene iniettato uno script JavaScript che gestisce gli eventi di dragover, dragleave e drop. Nella fase di dragover, si rende più evidente il bordo tratteggiato, magari aumentandone lo spessore e aggiungendo un’ombra. In dragleave, si rimuovono questi effetti per riportare lo stile alla normalità. Quando l’utente rilascia i file con drop, si modifica di nuovo lo stile per togliere l’effetto di evidenziazione e si assegnano i file trascinati all’input interno del componente Upload nascosto. Questi file vengono passati a Upload forzando la sua logica interna con uploadEl._onFileInputChange, seguita dalla chiamata a uploadEl.uploadFiles, cosicché Vaadin faccia partire in modo ufficiale il caricamento.
Mentre i file vengono caricati, il web component <vaadin-upload> emette eventi lato client, come upload-start, upload-progress, upload-success e upload-error. È in questi eventi che si mostra o si aggiorna una finestra modale creata dinamicamente con JavaScript e posizionata in overlay a schermo intero, così da coprire visivamente qualsiasi altro elemento della pagina. In upload-start, si crea il div a tutto schermo e lo si riempie con un messaggio iniziale. Con upload-progress, si ottengono i byte caricati e totali, che servono a calcolare la percentuale e a mostrarla in tempo reale. Quando arriva upload-success, si aggiorna il testo dell’overlay comunicando che il caricamento è concluso, mentre con upload-response si rimuove del tutto l’overlay. Se avviene un errore (upload-error), si cambia il testo in “Caricamento fallito!” e si fa sparire la finestra dopo un paio di secondi.
Dal lato server, si ascolta soprattutto addAllFinishedListener, che si attiva dopo che tutti i file sono stati processati da Vaadin, indipendentemente dal fatto che abbiano avuto successo o meno. In quell’evento si può per esempio eseguire un piccolo snippet JavaScript che rimuove o nasconde ogni overlay rimasto, nel caso in cui ci siano ancora elementi di interfaccia aperti. Subito dopo, si mostra una Notification di conferma, così l’utente sa che i file sono stati effettivamente ricevuti e che il server ha terminato l’elaborazione. Se uno qualunque dei file fallisce, si riceve addFailedListener, dal quale è possibile aprire una Notification personalizzata di errore.
In questo modo, si ottiene un sistema di caricamento massivo che fornisce un feedback chiaro sia a livello client (con la finestra di caricamento a schermo intero), sia a livello server (con la certezza che l’upload è terminato o che si è verificato un fallimento). L’utente non deve attendere senza indicazioni e non può cliccare altrove per sbaglio, poiché l’overlay copre ogni altro elemento, rendendo più fluida e sicura l’esperienza di caricamento di file anche di grandi dimensioni.
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.notification.Notification.Position;
import com.vaadin.flow.component.notification.NotificationVariant;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.upload.Upload;
import com.vaadin.flow.component.upload.receivers.MultiFileMemoryBuffer;
import com.vaadin.flow.router.Route;
@Route("")
public class DragDropView extends VerticalLayout {
private final MultiFileMemoryBuffer buffer = new MultiFileMemoryBuffer();
public DragDropView() {
// L'intero layout occupa tutto lo schermo
setSizeFull();
// 1. Creo il componente Upload, che però nasconderò
Upload upload = new Upload(buffer);
upload.setDropAllowed(false);
upload.setAutoUpload(true);
upload.getStyle().set("display", "none"); // Rimane "invisibile" all’utente
// 2. Creo l’area su cui l’utente farà drop dei file
VerticalLayout formItem = new VerticalLayout();
formItem.getStyle().set("border", "2px dashed #999") // Bordo tratteggiato grigio
.set("border-radius", "8px") // Angoli arrotondati
.set("padding", "40px") // Spazio interno
.set("box-sizing", "border-box") // Per sicurezza, calcolo dimensioni
.set("text-align", "center") // Testo centrato
.set("transition", "border-color 0.2s"); // Transizione morbida del bordo
formItem.setSizeFull();
// 3. Inietto lo script JavaScript che gestisce drag & drop e overlay
formItem.getElement().executeJs("""
const dropArea = this; // Riferimento all'elemento <div> di Vaadin
const uploadEl = $1; // Riferimento al <vaadin-upload> nascosto
// Funzioni e variabili per l'overlay
let modalDiv = null;
let modalContent = null;
function createModal() {
// Crea un div a tutto schermo
modalDiv = document.createElement('div');
modalDiv.classList.add('uploadProgressDiv'); // se volessimo stile condiviso
modalDiv.style.position = 'fixed';
modalDiv.style.top = '0';
modalDiv.style.left = '0';
modalDiv.style.width = '100%';
modalDiv.style.height = '100%';
modalDiv.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
modalDiv.style.display = 'flex';
modalDiv.style.justifyContent = 'center';
modalDiv.style.alignItems = 'center';
modalDiv.style.zIndex = '999999';
// Contenitore interno
modalContent = document.createElement('div');
modalContent.style.backgroundColor = '#fff';
modalContent.style.padding = '20px';
modalContent.style.borderRadius = '4px';
modalContent.style.minWidth = '300px';
modalContent.style.minHeight = '80px';
modalContent.style.display = 'flex';
modalContent.style.flexDirection = 'column';
modalContent.style.justifyContent = 'center';
modalContent.style.alignItems = 'center';
modalContent.innerHTML = 'Inizializzazione...';
modalDiv.appendChild(modalContent);
document.body.appendChild(modalDiv);
}
function updateModal(text) {
if (modalContent) {
modalContent.innerHTML = text;
}
}
function removeModal() {
if (modalDiv) {
document.body.removeChild(modalDiv);
modalDiv = null;
modalContent = null;
}
}
// Eventi nativi del web component <vaadin-upload>
// 1) L'upload inizia
uploadEl.addEventListener('upload-start', e => {
createModal();
updateModal('Caricamento iniziato...');
});
// 2) Progresso di caricamento
uploadEl.addEventListener('upload-progress', e => {
const loaded = e.detail.file.loaded;
const total = e.detail.file.size;
let percent = 0;
if (total > 0) {
percent = Math.round((loaded / total) * 100);
}
updateModal('Caricamento: ' + percent + '%');
});
// 3) Caricamento concluso con successo
uploadEl.addEventListener('upload-success', e => {
updateModal('Caricamento completato. Attendere la risposta del server...');
});
// 4) Caricamento terminato (subito dopo success/error)
uploadEl.addEventListener('upload-response', e => {
removeModal();
});
// 5) Se errore
uploadEl.addEventListener('upload-error', e => {
updateModal('Caricamento fallito!');
setTimeout(() => removeModal(), 2000);
});
// Gestione dell’area di drag & drop personalizzata
dropArea.addEventListener('dragover', (evt) => {
evt.preventDefault();
// Aggiungiamo un bordo più "accentuato"
dropArea.style.border = '4px dashed var(--lumo-primary-color)';
dropArea.style.borderRadius = '8px';
dropArea.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.3)';
});
dropArea.addEventListener('dragleave', (evt) => {
evt.preventDefault();
// Torniamo allo stato normale
dropArea.style.border = '2px dashed #999';
dropArea.style.borderRadius = '8px';
dropArea.style.boxShadow = 'none';
});
dropArea.addEventListener('drop', (evt) => {
evt.preventDefault();
// Rimuoviamo l'effetto di accentuazione
dropArea.style.border = 'none';
dropArea.style.boxShadow = 'none';
const dtFiles = evt.dataTransfer.files;
if (dtFiles.length > 0) {
const hiddenInput = uploadEl.$.fileInput;
hiddenInput.files = dtFiles;
// Forziamo la logica interna
if (uploadEl._onFileInputChange) {
uploadEl._onFileInputChange({ target: hiddenInput });
} else {
hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
}
// Avviamo l’upload
if (typeof uploadEl.uploadFiles === 'function') {
uploadEl.uploadFiles();
}
}
});
""", formItem.getElement(), upload.getElement());
// 4. Aggiungo i componenti alla vista
add(upload, formItem);
// Eventuali listener lato server
upload.addAllFinishedListener(event -> {
UI.getCurrent().getPage().executeJs("const elements = document.getElementsByClassName('uploadProgressDiv');"
+ "for (let i = 0; i < elements.length; i++) {" + " elements[i].style.display = 'none';" + "}");
// Questo scatta quando TUTTI i file caricati sono finiti
// (success/fail) e Vaadin ha completato le elaborazioni.
// Possiamo gestire la logica post-upload, ad es. aprire un Dialog
Notification.show("Caricamento completato lato server!");
});
upload.addFailedListener(event -> {
Notification note = new Notification("Caricamento fallito.");
note.setPosition(Position.TOP_CENTER);
note.addThemeVariants(NotificationVariant.LUMO_ERROR);
note.open();
});
}
}