GrFileUpload
Accepts files via selection or drag-and-drop and shows their state.
Overview
When to take it
- the file leaves for the server at once —
actionsends the multipart itself,requestconnects an uploader of your own; - there are many files —
concurrencylimits the parallel requests,limittheir number; - the upload is long — progress per file,
hideProgressOnSuccessremoves the bar after a success; - a drop zone is needed — drag & drop from the system plus a choice with a button and from the keyboard;
- the look is entirely your own — the slot gives the zone to the consumer while leaving them all of the mechanics.
When to take something else
| Need | Take |
|---|---|
| The file is the value of a form and leaves with it | GrFormFile |
| The image has to be examined | GrImageViewer |
| Only a progress indicator is needed | GrProgressBar |
| A server error answer | GrResponseErrorBanner |
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrFileUpload>Label</GrFileUpload>Examples 3 each with a live demo and its code
Importing a bank statement
The statement is sent the moment it is chosen:
requestis the app’s own call, it reports progress throughctx.onProgress, honoursctx.signal, and its answer types thesuccessevent.acceptfilters the system dialog and checks a dropped file too;maxFileSizeadds a limit. The component emits its errors, and the app words them for the field byFileValidationError.code.Import transactionsBusiness account •••• 4821 Drop a statement here or choose a fileCSV or OFX export from your bank, up to 10 MBSupplier documents, one request each
upload-mode="per-file"gives every row its status, percent, «Cancel» and «Retry upload»: when the storage times out, one document is resent, not the whole set.concurrencykeeps two requests in flight,limitstops the seventh file before anything is sent, andpreviewdraws thumbnails for photographed documents.Supplier onboarding · Northwind TradersRegistration certificate, VAT certificate, bank letter, ID of the signatory Drop the documents here or choose themPDF or photos, up to 20 MB each, six at mostA logo row instead of a drop zone
The default slot replaces the zone whole and hands its whole state back: the chosen
files, the uploadstatewith its phase and percent,openDialogandremoveFile. The row draws the file at once and revokes its address when it changes; dropping a file on the row still works.Workspace logo NTInitials are shown until a logo is setPNG or SVG, square, up to 1 MB
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
modelValuev-model | File[] | undefined | undefined | The set of files — the one shown in the list and the one that will go into `retry`. The prop is **optional**: without it the component keeps the set itself. If it is passed, the set follows it, and the consumer can clear or replace the list from the outside. |
action | string | undefined | undefined | The address for a `multipart/form-data` POST by the built-in uploader. Needed when there is no `request`. |
request | GrFileUploadRequest<TResponse> | undefined | undefined | A custom uploader (axios, fetch) instead of `action`: it receives the files and `signal`, `extraData`, `onProgress`. Stronger than `action`. |
name | string | undefined | "file" | The name of the file field in the `FormData` of the built-in uploader. `file` by default. |
multiple | boolean | undefined | false | Choosing several files at once; without it `limit` has no effect. `false` by default. |
limit | number | undefined | undefined | The limit of files in one choice with `multiple`: beyond it come `exceed` and `error`, and there is no upload. |
beforeUpload | ((file: File) => boolean | Promise<unknown>) | undefined | undefined | A guard before sending. It stays a prop rather than an emit deliberately: an emit returns no value, and this callback is obliged to answer "let it through or not". The notifications — `exceed`, `success`, `error`, `progress` — are emits. |
validators | FileValidator[] | undefined | undefined | Checks of the set before sending, after `accept`: size, type, count. Violations arrive in `error` as one `FileValidationError`, and the upload does not start. |
accept | string | undefined | undefined | The W3C `accept` for the `<input type="file">` — and sugar over `acceptValidator(...)`. The attribute alone is not enough: it filters the system dialog but not drag & drop — anything at all can be dragged in. The same value therefore goes into the validators as well, as in `GrFormFile`. |
capture | "user" | "environment" | undefined | undefined | `capture` for the mobile camera or microphone. |
directory | boolean | undefined | false | Choosing a whole directory (`webkitdirectory`). Supported by Chromium and Safari. |
disabled | boolean | undefined | false | The control is unavailable: the dialog and drag and drop do not work. Also switched on by `GrFormField`. |
readonly | boolean | undefined | false | Read-only: the value is visible and leaves with the form, but is not edited. |
invalid | boolean | undefined | false | The visual and ARIA state of an error. |
required | boolean | undefined | false | A required field (`aria-required`). |
ariaLabel | string | undefined | undefined | The accessible name outside `GrFormField`. |
headers | Record<string, string> | undefined | undefined | The request headers of the built-in uploader — a token, for instance. Not used with `request`. |
withCredentials | boolean | undefined | false | Send cookies to another domain (`xhr.withCredentials`) with the built-in uploader. |
showFileList | boolean | undefined | false | The list of chosen files under the zone: the name, the size, removal, and in `per-file` the status and a retry. `false` by default. |
uploadExtraData | ((files: File[]) => GrFileUploadExtraData | undefined) | undefined | undefined | Additional request fields computed from the set: they go into the `FormData` beside the files, and with `request` they arrive in `ctx.extraData`. An exception from the callback goes to `error`. |
placeholder | string | undefined | undefined | i18n: the hint text in the default UI. |
showProgress | boolean | undefined | true | Whether to show the default progress bar (if the `progress` slot is not used). |
progressTone |
| "primary" | The colour tone of the progress bar in the `uploading` phase. |
progressLabel | string | undefined | undefined | The aria-label for the progress bar. |
hideProgressOnSuccess | number | undefined | 800 | How many ms after `success` to hide the progress bar. `0` — do not hide it. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The size of the default UI: the padding of the drop zone, the icon tile, the type size of the labels. |
uploadMode | GrFileUploadMode | undefined | "batch" | How the set of files leaves: - `batch` (the default) — the whole set in one request. There is nothing to say about an individual file, so it has no status either; - `per-file` — a request of its own for every file (`request` is called with an array of one file, the contract does not change). A status and a percentage of the row appear, along with `retryFile` and `abortFile`. |
concurrency | number | undefined | 3 | How many files are uploaded simultaneously in the `per-file` mode. |
preview | boolean | undefined | false | Thumbnails for `image/*` in the list of files. |
Slots
| Slot | Type | Description |
|---|---|---|
default |
| A completely custom upload zone: the control gives away all of its state. |
label | any | The heading of the zone instead of `placeholder`. |
tip | any | A caption under the heading: the limits on type and size. |
progress |
| A progress indicator of your own instead of the built-in one. |
Events
| Event | Signature | Description |
|---|---|---|
update:modelValue | (e: "update:modelValue", files: File[]): void | The set of files has changed: a new choice or a removal. Not to be confused with `change`. |
exceed | (e: "exceed", files: File[], limit: number): void | More files were chosen than `limit` allows. The upload does not start. |
success | (e: "success", payload: TResponse, file?: File | undefined): void | `file` arrives only in the `per-file` mode: in a batch there is nothing to report about. |
error | (e: "error", error: unknown, file?: File | undefined): void | The set did not leave or the sending failed: `limit`, the validators, `beforeUpload`, the network, the server response, cancellation. `file` is present only in `per-file`. |
progress |
| The sending progress in percent, from `0` to `100`. `file` is present only in `per-file`. |
change | (e: "change", files: File[]): void | The set has been sent: in `batch` after success, in `per-file` when the whole queue has finished. Not to be confused with `update:modelValue`. |
stateChange | (e: "stateChange", state: GrUploadState): void | The overall upload phase changed — `idle`, `uploading`, `success`, `error` — with the percentage. |
focus | (e: "focus", event: FocusEvent): void | Focus entered the zone. Moving between the field and the list buttons does not count as an event. |
blur | (e: "blur", event: FocusEvent): void | Focus left the zone entirely. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrFileUploadMode, GrFileUploadExtraDataValue, GrFileUploadExtraData } from '@feugene/granularity/components/GrFileUpload'Filedist/types/components/GrFileUpload/GrFileUpload.vue.d.ts
type GrFileUploadMode = 'batch' | 'per-file';type GrFileUploadExtraDataValue = string | Blob;type GrFileUploadExtraData = Record<string, GrFileUploadExtraDataValue | GrFileUploadExtraDataValue[]>;type GrFileUploadRequestCtx = {
signal: AbortSignal;
extraData?: GrFileUploadExtraData;
onProgress?: (info: GrUploadProgressInfo) => void;type GrFileUploadRequest<TResponse = unknown> = (files: File[], ctx: GrFileUploadRequestCtx) => Promise<TResponse>;
export interface GrFileUploadProps<TResponse = unknown> {interface GrFileUploadProps<TResponse = unknown> {
modelValue?: File[];
action?: string;
request?: GrFileUploadRequest<TResponse>;
name?: string;
multiple?: boolean;
limit?: number;
beforeUpload?: (file: File) => boolean | Promise<unknown>;interface GrFileUploadEmits<TResponse = unknown> {
(e: 'update:modelValue', files: File[]): void;
(e: 'exceed', files: File[], limit: number): void;
(e: 'success', payload: TResponse, file?: File): void;
(e: 'error', error: unknown, file?: File): void;
(e: 'progress', percent: number, info?: GrUploadProgressInfo, file?: File): void;
(e: 'change', files: File[]): void;
(e: 'stateChange', state: GrUploadState): void;
(e: 'focus', event: FocusEvent): void;
(e: 'blur', event: FocusEvent): void;
}Filedist/types/components/GrFileUpload/fileEntry.d.ts
type GrFileUploadStatus = 'pending' | 'uploading' | 'success' | 'error';interface GrFileUploadEntry {
file: File;
status: GrFileUploadStatus;
percent: number;
error?: unknown;
}Filedist/types/components/GrFileUpload/config.d.ts
const grFileUploadConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrFileUpload">;Filedist/types/components/GrFileUpload/defaults.d.ts
interface GrFileUploadConfigurableProps {
size: GrComponentSize;
}Filedist/types/components/GrFileUpload/grFileUploadStyles.d.ts
type GrFileUploadSize = GrComponentSize;Filedist/types/components/GrFileUpload/safelist.d.ts
const grFileUploadSafelist: string[];Filedist/types/components/GrFileUpload/uploadViaXhr.d.ts
interface GrUploadProgressInfo {
percent: number;
loaded: number;
total: number;
indeterminate: boolean;
}class GrUploadAbortError extends Error {
constructor(message?: string);
}class GrUploadHttpError extends Error {
status: number;
payload: unknown;
constructor(status: number, payload: unknown);
}Filedist/types/components/GrFileUpload/uploadState.d.ts
type GrUploadPhase = 'idle' | 'uploading' | 'success' | 'error';interface GrUploadStateIdle {
phase: 'idle';
percent: 0;
indeterminate: false;
loaded: 0;
total: 0;
}interface GrUploadStateUploading {
phase: 'uploading';
percent: number;
indeterminate: boolean;
loaded: number;
total: number;
}interface GrUploadStateSuccess {
phase: 'success';
percent: 100;
indeterminate: false;
loaded: number;
total: number;
}interface GrUploadStateError {
phase: 'error';
percent: number;
indeterminate: false;
loaded: number;
total: number;
error: unknown;
}type GrUploadState = GrUploadStateIdle | GrUploadStateUploading | GrUploadStateSuccess | GrUploadStateError;Filedist/types/fileValidation/types.d.ts
type FileValidatorSource = 'input' | 'drop' | 'form';type FileValidator = (input: FileValidatorInput) => FileValidationIssue[] | Promise<FileValidationIssue[]>;Filedist/types/fileValidation/FileValidationError.d.ts
class FileValidationError extends Error {
code: FileValidationIssueCode;
file?: File;
issues: FileValidationIssue[];
constructor(issues: FileValidationIssue[], files?: File[], message?: string);
}Filedist/types/components/GrFileUpload/index.d.ts
type GrFileUploadInstance = ComponentExposed<typeof GrFileUploadComponent>;Accessibility
APG pattern
—
Full contract of the package: docs/keyboard.md ↗
Usage from the library prose
accept filters the dialog, a validator filters the drop
accept goes both to the <input type="file"> and into the chain as the first validator.
The attribute alone is not enough: it limits the system dialog, but anything at all can be
dragged into the zone — without the validator an extra file would reach the server.
<GrFileUpload accept="image/*,.pdf" :request="upload" multiple show-file-list />Beside it are capture (the camera on mobile) and directory (webkitdirectory,
Chromium and Safari).
The component does not pass fallthrough attributes on to the input: class and style
belong to the zone, and inheritAttrs: false would take them off it. The attributes of the
input itself are declared as props explicitly.
The set of files: remove and retry
showFileList shows the chosen files; each has a remove button, and retry() repeats the
upload of the current set — after an error there is no need to choose the files again.
const uploader = ref<GrFileUploadInstance>()
uploader.value?.retry()
uploader.value?.removeFile(file) // removing aborts the upload in flight: it was about the previous set
uploader.value?.abort()Removing the last file returns the state to idle: there is no point showing an error
about a set that no longer exists.
action or request, but not both
Where the files go is set by exactly one of two props: action — a URL for a multipart POST,
request — an uploader of your own. Pass both, and request does the sending while
action takes part in nothing.
The “exactly one” requirement cannot be expressed by a type, and that is a limitation of Vue
rather than an oversight: defineProps accepts an object type or an interface but not a
discriminated union — the keys are flattened into one flat object, and vue-tsc lets through
both “both at once” and “neither”. Both mistakes are therefore caught in a dev build with a
warning in the console: without it the first would surface at the first choice of a file,
and the second would not surface at all — a silently ignored action is indistinguishable
in behaviour from one that was honoured.
The upload modes
uploadMode decides what exactly the set of files is:
batch(the default) — the whole set leaves in one request (request(files, ctx)). There is one state for the set: there is nothing to say about an individual file, and it has no status;per-file— a request of its own for every file.requestis then called with an array of one file: the contract does not change, and the consumer’s uploader (axios and the rest) keeps working with no edits.
In the per-file mode a row gets a status (pending/uploading/success/error) and a
percentage, and the component gets retryFile(file) and abortFile(file):
<GrFileUpload
upload-mode="per-file"
:concurrency="3"
:request="upload"
multiple
show-file-list
/>concurrency (3 by default) limits the number of simultaneous requests: “per file”
without it would mean “all at once”, and a hundred files would open a hundred connections.
The summary state is computed by the worst outcome: uploading while at least one is uploading; an error if after the finish there is at least one; a success when all are successful. The percentage is the mean across the files: a bar weighted by bytes would jerk backwards when a large file starts after a small one.
Cancelling one file is not an error: the row returns to the queue, and it can be retried or removed.
While a file is on its way, its row has one button — “stop” (the circle-stop icon,
the name “Cancel upload of …”), and “remove” appears once the upload has been stopped or
has finished: two identical crosses side by side cannot be told apart. A row of the list
takes the full width of the column: the name is on the left and is truncated with an
ellipsis (the full one is in title), the status and the buttons stand at a shared right
edge — and in a narrow container a long name does not push them out.
The success, error and progress emits receive an optional trailing file argument —
it exists only in the per-file mode.
The progress does not disappear instantly
The bar built on GrProgressBar shows itself while the sending is in
progress, and after a success it holds for another hideProgressOnSuccess milliseconds —
800 by default. The delay is not cosmetic: a bar that vanishes in the same frame in which
it reached the end reads as “the upload failed” rather than as “done”. 0 leaves the bar
forever — that is the mode for a UI of your own that removes it itself.
showProgress: false switches the default bar off entirely: it is not needed when the
progress is drawn by the #progress slot or when the files are small and the bar only has
time to blink. progressTone colours it during the sending phase, and progressLabel
gives it a name for a screen reader.
The server's answer
The component is generic over the answer: TResponse is inferred from request and types
the payload of the success event — the any has left the public signature.
async function upload(files: File[]): Promise<UploadedFile> { /* … */ }
function onUploaded(file: UploadedFile) { /* the payload is typed */ }<GrFileUpload :request="upload" @success="onUploaded" />The type parameter is set exactly this way, through request: in an SFC template the
syntax <GrFileUpload<UploadedFile>> does not exist — that is TSX, not a Vue template. The
action branch does not govern the type of the answer: only the consumer knows what the
server will return.
The preview
preview draws a thumbnail for image/* files in the list. The link
(URL.createObjectURL) is revoked when a file is removed, when the set changes and on
unmount: without that the blob hangs in the memory of the tab until a reload.
The thumbnail is decorative (alt="") — the name of the file is already beside it, and
there is no point duplicating it for a screen reader.
A controlled set
<GrFileUpload v-model="files" :request="upload" show-file-list />modelValue is optional: without it the component keeps the set itself. If it is
passed, the set follows the prop and can be replaced or cleared from the outside, for
instance after the form has been submitted.
update:modelValue is emitted when the set itself changes: a new choice or the removal of
a file. It is not to be confused with change — that one means “the upload has finished”,
and that is a different moment.
The difference from GrFormFile remains one of purpose rather than of having a model:
GrFileUpload is about sending (action/request, progress, retry, per-file statuses),
GrFormFile is about a form field.
action and request are a boundary of another kind: if both are set, request works; if
neither is set, a dev build warns on mounting rather than at the first choice of a file.
Expressing the requirement with a type (a discriminated union) is impossible in an SFC:
defineProps accepts only an object type or an interface.
The guard and the notifications
beforeUpload is a prop callback: it has to return “let it through or not”, and an emit
returns no value. Everything else is emits: exceed (there are more files than limit),
progress, success, error, change, stateChange.
:on-exceed="fn" keeps working: in Vue an emit also arrives as a listener prop.
Races and loose ends
- Two quick choices in a row overlap (the validators are asynchronous). The last one is always the current one: a run has a number, and the one that fell behind quietly leaves the race without cutting its neighbour’s upload short.
- On unmount the component aborts the active XHR and clears the timer that hides the success — otherwise the upload would keep going and the timer would jerk the state of a destroyed instance.
- A custom
requestis not obliged to callonProgress. The total volume is then taken from the sizes of the files rather than from zero: “100%” withtotal: 0would be read by the consumer as “zero uploaded”.
Accessibility
The accessible control is the native <input type="file"> itself; the zone gets no widget
role, otherwise the input inside it is lost for a screen reader (nested-interactive). The
zone shows the focus of the input through focus-within, and disabled is dimmed with a
background rather than with opacity: transparency dilutes text tokens tuned to AA.
disabled and readonly arrive both from GrFormField and from GrForm — not only from
the props of the component. readonly really does forbid input: the set is visible and
leaves with the form, but the dialog will not open and a drop will not be accepted. HTML
has no readonly attribute for <input type="file">, so the system dialog is suppressed by
cancelling the default action — the input stays in the Tab order and is announced as
read-only.
The phases of the upload are announced by a live region (role="status") that exists from
the first render: a region that appears already carrying text is not announced at all by
some AT. The progress as a percentage does not go there — a screen reader would choke.
Props in this example