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