← Overview

fileUpload

A native file field in a box that says what is attached to it — droppable as a whole, with one row per file and a way to take a single one back out. Give it an endpoint and every row gets a progress bar.

Several files: PDFs and images

Drop a few files into the box, or use the button. Each one gets a row with its name and its size — the whole of what the native control does not show, which reports “3 files” and names none of them. Press the cross on a row and that one file leaves the selection: the field's files is rewritten through a DataTransfer, which is the platform's own way of doing it, and a change event goes out exactly as it does after the picker.

Nothing is uploaded here. There is no data-fileupload-url on this box, so the files travel with the form the way they always did — and that is why there is no progress bar in this half of the page: a submitted form tells the page nothing while it uploads.

PDF or image files, as many as you like.

You can also drop them anywhere in this box.

One file, and a type that is turned away

No multiple, so a drop replaces what is there with the first file it carries. And accept=".pdf": drop something else — a screenshot, a zip — and it is reported rather than swallowed. The field's own attribute decides that, not a rule this component invented: a file the picker would never have offered has no business arriving through the back door. It stays a filter and not a verdict, though — what is inside a file is the server's business.

One PDF.

Or drop the file in this box.

Inside a form: required, and reset

Press Send with an empty field and the browser says so itself — required is the platform's, and this lane adds no validation of its own. Then attach something and press Reset: a reset empties the field without firing a change event, the one value change the module cannot hear, so it listens for reset on the form and rebuilds the list on the next frame.

Or drop the file in this box.

A limit the platform does not have

data-fileupload-maxsize="2MB" per file and data-fileupload-maxtotal="5MB" for the selection, both on the field, next to accept — because that is where every other constraint on this value already lives. There is no native maxsize, so this is the one rule the component adds; it adds it through the platform, with setCustomValidity(), which is the same channel formValidation uses for its own non-native rule.

Attach something over 2 MB. The file stays — taking away what you just chose would leave you with no explanation and, in a single-file field, with “no file chosen” — but its row is marked, the status line names the limit, and Send is refused by the browser itself with that same sentence on the field. Remove the row, or attach something smaller, and the form goes through. The running total underneath states the budget while there is still room in it. Watch what the upload lane does with the same limit further down: there the file is refused at the door instead, because a row there means a transfer.

Images, up to 2 MB each and 5 MB in total.

You can also drop them in this box.

With an endpoint: a progress bar per file

Everything below carries one extra attribute — data-fileupload-url — and nothing else changes: same box, same markup, same stylesheet. What changes is who carries the bytes. The module uploads each file the moment it is chosen, so it can report how far it has got; the form ends up carrying only the handles the endpoint returned.

These examples need a server that accepts a POST, which no static host does — neither VS Code's Live Server nor the online demo on Codeberg Pages. To see the bars move, clone the project and open this page from the server that ships with it: npm run demo:upload, then http://127.0.0.1:8123/src/components/fileUpload/demo.html. Anywhere else every row below turns red and says failed — the file was picked, queued and sent, and the server turned it away (a plain static server answers 405 Method Not Allowed). The component reports the status it got; look in the console.

Several files, three at a time

Drop a few files into the box, or use the button. Each one starts moving immediately and gets a bar of its own; at most three transfer at once, because twenty parallel uploads on one line finish later than three at a time and report nonsense on the way. The cross cancels a transfer while it runs — xhr.abort(), so the socket really closes — and removes the file once it is up.

Watch the tail end of each bar: when the last byte has left the browser the bar goes indeterminate and the row says processing. That is the truth about upload progress that most bars gloss over — 100 % means "sent", not "stored", and this demo endpoint waits 600 ms before answering so the difference is visible.

Images or PDFs, up to 10 MB each.

You can also drop them anywhere in this box.

What the form would send: nothing yet.

What the form actually carries

The line above is printed from that form itself on every change. Attach something and watch it: no file ever appears in it. The bytes went out one by one while you were still filling the form in, and what is left to submit is attachmentIds=demo-1 — the handle this demo endpoint returned. Send is therefore instant, which is the whole reason to upload early.

Press Send while something is still uploading and the browser refuses, with a message on the field. There is no submit handler in this component: the field's own validity says why — the same channel the size limits use in the default lane, so formValidation would render it with no extra wiring.

When it fails: retry, and the file that is still here

This box posts to /dev/upload?fail=1, which answers 500 with a JSON error. The row turns red, says failed, quotes the server's own sentence in the status line — and offers Retry, because the row still holds the File. Nobody has to find that file on their disk a second time. The form stays blocked until every failed row is retried or removed.

One file — this endpoint refuses it on purpose.

Or drop it in this box.

One file at a time, and one slow line

No multiple, so a second pick replaces the first — including the file already on the server, which is deleted through data-fileupload-delete-url. And ?slow=300, which makes this endpoint read each chunk with a 300 ms pause: a genuinely slow line, so the bar creeps rather than flashes. Cancel it halfway and watch the request end in the server's console.

One image, up to 4 MB.

Or drop it in this box.

The same component, retuned

One instance with four of its knobs set from outside — --fileUpload-zone-style: solid, a quieter edge, --fileUpload-padding and --fileUpload-radius. Nothing about the markup or the behaviour changes: the token block at the top of fileUpload.scss is the whole override surface, and it is declared on the component's own root, which is where an override has to sit to win.

Drop them here.