Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions articles/components/upload/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,9 @@ The user interface, including the Upload component, should take this into accoun

|===

|<<modular-upload#,Modular Upload>>
|Independent upload components for flexible layouts and embedded upload experiences.

|<<../progress-bar#,Progress Bar>>
|Component for showing task completion progress.

Expand Down
217 changes: 217 additions & 0 deletions articles/components/upload/modular-upload.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
---
title: Modular Upload
page-title: Modular Upload with Upload Manager | Vaadin components
description: Build modular file upload experiences with independent components and flexible layouts.
meta-description: Learn how to create modular file upload experiences in Vaadin using the Upload Manager and composable upload components.

Check failure on line 5 in articles/components/upload/modular-upload.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'composable'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'composable'?", "location": {"path": "articles/components/upload/modular-upload.adoc", "range": {"start": {"line": 5, "column": 110}}}, "severity": "ERROR"}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add [cC]omposable to the vocabulary, as apparently Vale's dictionary doesn't recognize it.

section-nav: badge-preview
---
= [since:com.vaadin:vaadin@V25.1]#Modular Upload#

:preview-feature: Modular Upload
:feature-flag: com.vaadin.experimental.modularUpload
include::{articles}/_preview-banner.adoc[opts=optional]

The standard <<index#,Upload>> component bundles a button, drop zone, file list, and upload management into a single component. [classname]`UploadManager` decouples these concerns into independent, composable pieces. Use it when you need to customize the upload layout, place the file list in a different location, or integrate upload functionality into a custom component such as a chat input, form field, or toolbar.

Check failure on line 14 in articles/components/upload/modular-upload.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'composable'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'composable'?", "location": {"path": "articles/components/upload/modular-upload.adoc", "range": {"start": {"line": 14, "column": 199}}}, "severity": "ERROR"}

[classname]`UploadManager` is non-visual. It handles file validation, upload queue management, and server communication. Three UI components connect to it:

[classname]`UploadButton`:: A button that opens the file picker dialog. Automatically disables when the maximum file count is reached.
[classname]`UploadDropZone`:: A drag-and-drop target area that can wrap other content.
[classname]`UploadFileList`:: Displays files with progress indicators, status messages, and action buttons (retry, abort, remove).

Check warning on line 20 in articles/components/upload/modular-upload.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vaadin.Terms] Prefer 'stop', 'exit', 'cancel', or 'end' over 'abort'. Raw Output: {"message": "[Vaadin.Terms] Prefer 'stop', 'exit', 'cancel', or 'end' over 'abort'.", "location": {"path": "articles/components/upload/modular-upload.adoc", "range": {"start": {"line": 20, "column": 116}}}, "severity": "WARNING"}

Each component can be placed anywhere in your layout, independently of one another. They all link to the same [classname]`UploadManager` instance, which coordinates their behavior.


== Basic Usage

Create an [classname]`UploadManager` instance and pass it to the UI components. The components automatically synchronize with the manager's state.

[.example,themes="lumo,aura"]
--
ifdef::lit[]
[source,typescript]
----
include::{root}/frontend/demo/component/upload/upload-manager-basic.ts[render,tags=snippet,indent=0,group=Lit]
----
endif::[]

ifdef::flow[]
[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/component/upload/UploadManagerBasic.java[render,tags=snippet,indent=0,group=Flow]
----
endif::[]

ifdef::react[]
[source,tsx]
----
include::{root}/frontend/demo/component/upload/react/upload-manager-basic.tsx[render,tags=snippet,indent=0,group=React]
----
endif::[]
--

The [classname]`UploadManager` is created with an upload handler that processes the files. The [classname]`UploadButton` and [classname]`UploadFileList` components are linked to the manager and can be placed anywhere in the layout.


ifdef::flow[]
== Owner Component Lifecycle

The [classname]`UploadManager` requires an owner component, typically the view or layout containing the upload UI. The manager's lifecycle is tied to the owner's lifecycle:

- When the owner is detached from the UI or disabled, uploads stop working
- Events fired by the manager use the owner as their source component

[source,java]
----
// The view itself is typically used as the owner
public class MyView extends VerticalLayout {
public MyView() {
var manager = new UploadManager(this, handler);
// ...
}
}
----

This design ensures that the upload infrastructure is automatically cleaned up when the view is removed, preventing resource leaks.
endif::[]


== Using a Drop Zone

The [classname]`UploadDropZone` component provides drag-and-drop functionality. It can wrap other content, allowing users to drop files onto a larger area.

[.example,themes="lumo,aura"]
--
ifdef::lit[]
[source,typescript]
----
include::{root}/frontend/demo/component/upload/upload-manager-drop-zone.ts[render,tags=snippet,indent=0,group=Lit]
----
endif::[]

ifdef::flow[]
[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/component/upload/UploadManagerDropZone.java[render,tags=snippet,indent=0,group=Flow]
----
endif::[]

ifdef::react[]
[source,tsx]
----
include::{root}/frontend/demo/component/upload/react/upload-manager-drop-zone.tsx[render,tags=snippet,indent=0,group=React]
----
endif::[]
--

The drop zone applies a `dragover` attribute when files are dragged over it, which can be styled using CSS.


== Thumbnails

The [classname]`UploadFileList` supports a thumbnails theme variant that displays uploaded files as a compact grid instead of a vertical list. Image files are shown with a thumbnail preview, while other file types display a file icon.

[.example,themes="lumo,aura"]
--
ifdef::lit[]
[source,typescript]
----
include::{root}/frontend/demo/component/upload/upload-manager-thumbnails.ts[render,tags=snippet,indent=0,group=Lit]
----
endif::[]

ifdef::flow[]
[source,java]
----
include::{root}/src/main/java/com/vaadin/demo/component/upload/UploadManagerThumbnails.java[render,tags=snippet,indent=0,group=Flow]
----
endif::[]

ifdef::react[]
[source,tsx]
----
include::{root}/frontend/demo/component/upload/react/upload-manager-thumbnails.tsx[render,tags=snippet,indent=0,group=React]
----
endif::[]
--


== Configuration

The [classname]`UploadManager` accepts an <<file-handling#,upload handler>> for processing files, and shares most of its configuration API with the standard <<index#,Upload>> component -- including <<index#upload-restrictions,upload restrictions>> (file count, size, and format), <<index#auto-upload,auto-upload>>, and <<index#listeners,event listeners>>.

The following sections describe where [classname]`UploadManager` differs from [classname]`Upload`.


=== File Type Restrictions

Unlike [classname]`Upload`, which uses a single [methodname]`setAcceptedFileTypes()` method for both MIME types and extensions as a client-side hint, [classname]`UploadManager` provides two separate methods:

[source,java]
----
// Accept by MIME type (wildcards supported)
manager.setAcceptedMimeTypes("image/*", "application/pdf");

// Accept by file extension (must start with a dot)
manager.setAcceptedFileExtensions(".pdf", ".docx", ".xlsx");
----

When both are configured, a file must match at least one MIME type and at least one file extension. These restrictions are enforced both on the client side (to filter the file picker) and on the server side (to reject non-matching uploads). This is an improvement over [classname]`Upload`, which only validates file types on the client.



=== Event Differences

The [classname]`FileRejectedEvent` in [classname]`UploadManager` carries a typed [classname]`FileRejectionReason` enum (`TOO_MANY_FILES`, `FILE_TOO_LARGE`, `INCORRECT_FILE_TYPE`) instead of a raw error string, making it easier to handle specific rejection causes programmatically.

Upload progress events aren't available as component event listeners. Use [classname]`TransferProgressListener` on the <<file-handling#,upload handler>> instead.


ifdef::flow[]
== Disabling Uploads

[WARNING]
====
Disabling UI components ([classname]`UploadButton`, [classname]`UploadDropZone`) only affects the user interface. A malicious client can still initiate uploads by sending requests directly to the server. To securely prevent uploads, you must disable the [classname]`UploadManager` itself.
====

Use [methodname]`setEnabled()` on the manager to securely control whether uploads are allowed. Disabling the manager prevents the server from accepting upload requests, regardless of the state of UI components. You can also disable individual UI components for visual feedback, but this should always be combined with disabling the manager for security.
endif::[]


== Internationalization

The [classname]`UploadFileList` component supports internationalization (i18n) for its labels and messages:

ifdef::flow[]
[source,java]
----
var i18n = new UploadFileListI18N()
.setFile(new UploadFileListI18N.File()
.setRetry("Try again")
.setStart("Upload")
.setRemove("Delete"))
.setError(new UploadFileListI18N.Error()
.setTooManyFiles("Too many files")
.setFileIsTooBig("File is too large")
.setIncorrectFileType("Invalid file type"));

fileList.setI18n(i18n);
----
endif::[]


== Related Components

|===

|<<index#,Upload>>
|The default Upload component with built-in drop zone, button, and file list.

|<<../progress-bar#,Progress Bar>>
|Component for showing task completion progress.

|===


[discussion-id]`UPLOAD-MANAGER-MODULAR-UPLOAD`
47 changes: 47 additions & 0 deletions articles/components/upload/styling.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
|Removes border around the upload component
|Aura

|`thumbnails`
|Displays file list items as a compact grid with thumbnail previews for images. Apply to [classname]`UploadFileList`.
|Aura, Lumo

|===


Expand Down Expand Up @@ -221,3 +225,46 @@
Progress bar track:: `vaadin-upload-file+++<wbr>+++** > vaadin-progress-bar::part(bar)**`
Progress bar indicator:: `vaadin-upload-file+++<wbr>+++** > vaadin-progress-bar::part(value)**`
Indeterminate progress bar (unknown estimate):: `vaadin-upload-file+++<wbr>+++** > vaadin-progress-bar[indeterminate]**`


== Modular Upload Components

:preview-feature: Modular Upload
:feature-flag: com.vaadin.experimental.modularUpload
include::{articles}/_preview-banner.adoc[opts=optional]

The <<modular-upload#,modular upload>> components ([classname]`UploadButton`, [classname]`UploadDropZone`, [classname]`UploadFileList`) can be styled independently. The File List and File properties and selectors documented above apply equally to the standalone [classname]`UploadFileList` component.


=== Upload Button

[classname]`UploadButton` (`vaadin-upload-button`) is styled the same as a regular <<../button/styling#,Button>> component. It supports the same style variants, CSS custom properties, and CSS selectors.

Root element:: `vaadin-upload-button`

==== States

Disabled:: `vaadin-upload-button+++<wbr>+++**[disabled]**`
Focused:: `vaadin-upload-button+++<wbr>+++**[focused]**`
Keyboard focused:: `vaadin-upload-button+++<wbr>+++**[focus-ring]**`
Maximum files reached:: `vaadin-upload-button+++<wbr>+++**[max-files-reached]**`


=== Upload Drop Zone

[classname]`UploadDropZone` (`vaadin-upload-drop-zone`) is intentionally unstyled. It serves as a functional container that wraps your own content. A semi-transparent overlay is shown during drag-over to indicate the drop target.

Check failure on line 255 in articles/components/upload/styling.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'unstyled'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'unstyled'?", "location": {"path": "articles/components/upload/styling.adoc", "range": {"start": {"line": 255, "column": 74}}}, "severity": "ERROR"}

Root element:: `vaadin-upload-drop-zone`

==== States

File dragged over:: `vaadin-upload-drop-zone+++<wbr>+++**[dragover]**`

Check failure on line 261 in articles/components/upload/styling.adoc

View workflow job for this annotation

GitHub Actions / lint

[vale] reported by reviewdog 🐶 [Vale.Spelling] Did you really mean 'dragover'? Raw Output: {"message": "[Vale.Spelling] Did you really mean 'dragover'?", "location": {"path": "articles/components/upload/styling.adoc", "range": {"start": {"line": 261, "column": 59}}}, "severity": "ERROR"}
Disabled:: `vaadin-upload-drop-zone+++<wbr>+++**[disabled]**`
Maximum files reached:: `vaadin-upload-drop-zone+++<wbr>+++**[max-files-reached]**`


=== Upload File List

[classname]`UploadFileList` (`vaadin-upload-file-list`) uses the same `vaadin-upload-file` child elements as the standard Upload component. The <<File List Properties>>, <<File Properties>>, <<File List>>, <<File>>, <<File States>>, <<File Actions>>, and <<Progress Bar>> sections above all apply.

Root element:: `vaadin-upload-file-list`
4 changes: 4 additions & 0 deletions frontend/demo/component/upload/react/upload-demo-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,10 @@ window.customElements.whenDefined('vaadin-upload').then(() => {
Object.getPrototypeOf(document.createElement('vaadin-upload'))._createXhr = mockXhrGenerator;
});

// Monkey-patch UploadManager prototype to use MockHttpRequest
import { UploadManager } from '@vaadin/upload/vaadin-upload-manager.js';
(UploadManager.prototype as any)._createXhr = mockXhrGenerator;

declare global {
interface Window {
OriginalHttpRequest?: typeof XMLHttpRequest;
Expand Down
32 changes: 32 additions & 0 deletions frontend/demo/component/upload/react/upload-manager-basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import './upload-demo-helpers'; // hidden-source-line
import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line
import React from 'react';
import { UploadButton } from '@vaadin/react-components/UploadButton.js';
import { UploadFileList } from '@vaadin/react-components/UploadFileList.js';
import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js';
import { UploadManager } from '@vaadin/upload/vaadin-upload-manager.js';

function Example() {
// tag::snippet[]
// Create the upload manager
const manager = React.useMemo(
() =>
new UploadManager({
target: '/api/fileupload',
maxFiles: 5,
maxFileSize: 10 * 1024 * 1024, // 10 MB
accept: 'image/*,application/pdf',
}),
[]
);

return (
<VerticalLayout theme="spacing">
<UploadButton manager={manager}>Select Files</UploadButton>
<UploadFileList manager={manager} style={{ width: '100%' }} />
</VerticalLayout>
);
// end::snippet[]
}

export default reactExample(Example); // hidden-source-line
52 changes: 52 additions & 0 deletions frontend/demo/component/upload/react/upload-manager-drop-zone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import './upload-demo-helpers'; // hidden-source-line
import '@vaadin/icons';
import { reactExample } from 'Frontend/demo/react-example'; // hidden-source-line
import React from 'react';
import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js';
import { Icon } from '@vaadin/react-components/Icon.js';
import { UploadButton } from '@vaadin/react-components/UploadButton.js';
import { UploadDropZone } from '@vaadin/react-components/UploadDropZone.js';
import { UploadFileList } from '@vaadin/react-components/UploadFileList.js';
import { VerticalLayout } from '@vaadin/react-components/VerticalLayout.js';
import { UploadManager } from '@vaadin/upload/vaadin-upload-manager.js';

function Example() {
// tag::snippet[]
// Create the upload manager
const manager = React.useMemo(
() =>
new UploadManager({
target: '/api/fileupload',
maxFiles: 10,
}),
[]
);

return (
<VerticalLayout theme="spacing" style={{ width: '100%' }}>
<UploadDropZone
manager={manager}
style={{
border: '1px dashed var(--vaadin-border-color-secondary)',
borderRadius: 'var(--vaadin-radius-l)',
padding: 'var(--vaadin-padding-l)',
width: '100%',
boxSizing: 'border-box',
}}
>
<HorizontalLayout
theme="spacing"
style={{ alignItems: 'center', justifyContent: 'center' }}
>
<Icon icon="vaadin:upload" />
<span>Drop files here or</span>
<UploadButton manager={manager}>Browse</UploadButton>
</HorizontalLayout>
</UploadDropZone>
<UploadFileList manager={manager} style={{ width: '100%' }} />
</VerticalLayout>
);
// end::snippet[]
}

export default reactExample(Example); // hidden-source-line
Loading