File Display
FreeFile attachment item with a MIME-type-aware icon badge, formatted size, upload progress, success/error status and download/remove actions — in card, row or compact layouts.
Complexity tier: Interactive The primitive bar, plus declared async states, a keyboard model, focus restoration and undo for destructive actions.
import { SvFileDisplayComponent } from '@styloviz/commerce';Demo & usage
Basic usage
Usage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Uploading with progress
<sv-file-display fileName="video.mp4" mimeType="video/mp4" status="uploading" [progress]="62" />
Compact row, image preview
<sv-file-display fileName="cover.png" mimeType="image/png" previewUrl="/thumb/cover.png" variant="compact" />
A delete that failed
<sv-file-display [fileName]="file.name" [mimeType]="file.type" [removing]="deleting().has(file.name)" [removeError]="deleteError()" (remove)="onRemove($event)" />
Deleting a file
// There is no rollback() here, unlike every other write in the kit: this // component renders ONE file, and the list it sits in belongs to you, so it has // nothing to put itself back into. What it offers instead is the step before — // an inline confirmation, on by default. onRemove(event: FileRemoveEvent): void { this.deleteError.set(null); this.deleting.update(s => new Set(s).add(event.fileName)); // requestKey makes the delete idempotent: a retried request cannot delete // whatever has since taken the same name. this.api.deleteFile(event.fileName, event.requestKey).subscribe({ next: () => this.files.update(list => list.filter(f => f.name !== event.fileName)), error: () => this.deleteError.set('The file could not be deleted.'), complete: () => this.deleting.update(s => { const n = new Set(s); n.delete(event.fileName); return n; }), }); }
The download is not a safety claim
scan defaults to 'unscanned', which is not a synonym for 'clean'. An unlabelled one-click download on every file asserts, on your behalf, that the file is safe to open — the one claim a file viewer must never make by omission. 'pending' withholds the download until the check finishes; 'flagged' withholds it outright and says why, unless allowUnsafeDownload puts the bytes behind an explicit confirmation for the people who genuinely need them.
<sv-file-display [fileName]="file.name" [mimeType]="file.type" [scan]="file.scan" [scanNote]="file.scanNote" [allowUnsafeDownload]="user.canTriageQuarantine" (download)="fetch(file)" />
Retrying — or resuming — a failed upload
A failed upload used to say "Upload failed" beside the size and offer nothing: no reason, and no way to try again, so the only route back was to remove the row and pick the file a second time. The retry control states which of the two acts it performs, because a "Retry" that silently re-sends 900 MB over a metered connection is a different offer from resuming.
<sv-file-display [fileName]="file.name" [fileSize]="file.size" [status]="file.status" [uploadError]="file.error" [resumable]="true" [uploadedBytes]="file.sent" (retryUpload)="resume($event)" (cancelUpload)="abort($event.fileName)" /> // resumeFrom is 0 unless you said BOTH that the upload is resumable and how // far it got — either half missing means the bytes have to go again. resume(e: FileRetryEvent) { this.api.upload(e.fileName, { offset: e.resumeFrom, key: e.requestKey }); }
When to use
- Listing uploaded or attached files with type, size and per-item actions.
- You want a built-in upload progress bar and success/error states.
- For the upload dropzone itself, use the File Upload component.
Async states
None — this component has nothing to load, so it has no loading, empty or error state to drive.
Server writes
- Writes
- remove
- retryUpload
Each payload carries a
requestKey— send it with the request so a retry cannot apply the same change twice.- Rollback
- None — this component renders a single item out of a collection you own, so there is nothing here to put back. It asks for confirmation first instead, and the item only moves when you change the data.
- While saving
- Bind
removingto stop the action being repeated while the request is out. - On failure
- Pass the failure to
uploadErrorto show it in place — without taking away what the user was working on.
Inputs
| Name | Type | Default | Description |
|---|---|---|---|
| fileName | string | '' | File name including extension. |
| fileSize | number | undefined | undefined | File size in bytes (auto-formatted). |
| mimeType | string | '' | MIME type string for icon selection. |
| status | FileStatus | 'idle' | Current file status. |
| progress | number | undefined | undefined | Upload progress 0–100. Only shown when status = 'uploading'. |
| previewUrl | string | '' | Image preview URL (shown for image MIME types). |
| removable | boolean | true | Show remove button. |
| downloadable | boolean | true | Show download button. |
| variant | FileVariant | 'card' | Layout variant. |
| scan | FileScanState | 'unscanned' | What the host knows about this file's safety. |
| scanNote | string | '' | The host's own wording for a `'flagged'` file — what the scanner matched, or which policy it broke. Shown in place of the generic sentence, because "Contains a macro that writes to disk" is actionable and "Flagged as unsafe" is not. |
| allowUnsafeDownload | boolean | false | Whether a `'flagged'` file may be downloaded at all, behind a confirmation. Off by default, and the override is a HOST decision rather than a user one: an analyst triaging a quarantine queue needs the bytes, and an ordinary user of a file list does not. When it is off there is no button to press, no disabled control to hover, and no ambiguity about whether pressing harder would work — just the reason the file is blocked. |
| uploadError | SvDataError | string | null | null | Why the upload failed. Shown beside the retry control when `status` is `'error'`. |
| resumable | boolean | false | Whether the host can resume this upload rather than starting over. |
| uploadedBytes | number | undefined | undefined | Bytes already accepted by the server. Only meaningful with `resumable`, and it is what turns *Retry* into *Resume from 412 MB*. |
| confirmRemove | boolean | true | Ask before deleting. Defaults to ON, and the previous behaviour was a single click that emitted straight through. Deleting a file is not undoable by anything the component can offer — see FileRemoveEvent — and the button is a 28px × next to a download button of the same size, so the guard is the only thing standing between a misplaced click and a lost upload. Turn it off only where the host runs its own confirmation dialog. |
| removing | boolean | false | True while the delete is being sent. Both the confirm and cancel buttons stop responding, so the request cannot be fired twice. |
| removeError | SvDataError | string | null | null | The delete failed. Reported in place, because the row is still here — the host never took it out. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| remove | FileRemoveEvent | The user confirmed deleting the file. Bind this or nothing happens: the component only asks, it never deletes. |
| download | void | Emitted when the download button is clicked. |
| retryUpload | FileRetryEvent | Retry (or resume) a failed upload. Carries `resumeFrom`, which is `0` unless the host declared the upload resumable and said how far it got. |
| cancelUpload | { fileName: string } | Abandon an upload that is still running. |
Customization & theming
- Three layouts via `variant`: card, row, compact.
- Toggle the `removable` and `downloadable` actions independently.
- Drive the progress bar and status chip with `status` + `progress`.
- `confirmRemove` (on by default) asks inline before emitting `(remove)`. Set it to `false` when the host runs its own confirmation dialog, and the × emits straight through.
- `removing` locks the × and the confirm button while the delete is in flight; `removeError` reports a failed delete under the row, which is still there because the host never took it out.
- `(remove)` carries `{ fileName, requestKey }`. There is deliberately no `rollback()`: one file cannot put itself back into a list it does not own.
- `scan` (`unscanned` / `pending` / `clean` / `flagged`) drives both the disclosure and whether a download is offered at all; `scanNote` supplies your own wording and `allowUnsafeDownload` puts a flagged file behind a confirmation instead of withholding it.
- `status="error"` renders `uploadError` and a retry control. `resumable` + `uploadedBytes` turn it into "Resume from 412 MB" and put the offset in `(retryUpload)`; `(cancelUpload)` abandons one that is still running.
Best practices
- Do: Pass a real `mimeType` so the icon colour and image preview resolve correctly.
- Do: Wrap multiple items in a `role="list"` container so each file reads as a list entry.
- Tip: Hide actions you do not support with `removable`/`downloadable` rather than disabling them.
- Do: Leave `confirmRemove` on unless you run your own dialog. The × is a 28px target next to a same-size download button, and the delete is the one write here that nothing can undo.
- Do: Bind `removing` while the delete is in flight so it cannot be sent twice, and send `requestKey` so a retry cannot delete whatever has since taken the same name.
- Don't: Don't remove the row yourself before the server confirms. There is no `rollback()` here — the list is yours, so the component has nothing to put itself back into — and `removeError` reports the failure in place, on a row that is still there.
- Do: Set `scan` from what you actually know. Leaving it at `'unscanned'` is honest; setting it to `'clean'` because it looks tidier tells the user a scanner cleared a file no scanner ever saw.
- Do: Give a flagged file a `scanNote`. "Matched EICAR-Test-File" is actionable; "Flagged as unsafe" is not.
- Do: Bind `uploadError` alongside `status="error"`. The retry button appears either way, but a retry with no stated reason invites the same failure a second time.
- Don't: Don't set `resumable` unless the server really accepts a byte offset. The label promises the user their partial upload survives, and `resumeFrom` is the number that has to honour it.
- Don't: Don't leave `allowUnsafeDownload` on for ordinary users. It exists for an analyst triaging a quarantine queue, not for a file list.
Accessibility
- Each item is a labelled `group` named by its file; download/remove buttons include the file name so they are unambiguous in a list.
- The × keeps its place while the confirmation is open rather than being swapped out, so the focus that pressed it still has somewhere to be. It exposes `aria-expanded`, and its label becomes "Cancel removing <file>".
- The confirmation is a labelled `group` whose text says the delete cannot be undone — not colour alone. A failed delete renders as `role="alert"`.
- The progress bar is a labelled `progressbar` exposing its current value; success/error indicators are exposed as labelled images.
- Image previews require `alt` (derived from the file name).
- The scan state is written in words, not conveyed by a colour or a glyph alone — "Not checked for threats" and "Checked — no threats found" must not look like each other. In the compact layout, where there is no room for the sentence, the glyph carries it as its accessible name.
- A withheld download is absent rather than disabled, and the reason is stated beside it: a greyed control answers nothing and invites a harder press.
- The download button keeps its place while the unsafe-download confirmation is open, exposes `aria-expanded`, and the strip that follows it is the next tab stop.
Feedback on File Display
Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.