Upload
A file uploader that is useful before it touches the network. With no uploadUrl it is a picker: drag & drop (folders and pasted screenshots included), restrictions that stay on the row with their reason, image previews, and everything chosen exposed through [(value)] as plain File objects. Add a URL and the same element uploads — through a pluggable adapter, chunked and resumable on request.
XHR, not fetch: xhr.upload.onprogress is the only browser API that reports request-body progress, which is why every reference library uses it too. Apps whose auth lives in an interceptor can swap in createHttpClientUploadAdapter in one line.
There is no WAI-ARIA APG pattern for file upload, so the accessibility contract is assembled from primitives and written down: the real <input type="file"> stays in the accessibility tree, the drop zone is a button (so the keyboard reaches the same dialog and nothing nests inside an interactive element), the list is a list with a roving tab stop, Delete removes the focused row, and every change is announced politely. Full API reference →
The demos below run against an in-browser FakeUploadServer, not the network. This site is a static deploy, so a real POST /api/upload would be answered by the SPA rewrite with a 200 — a demo on the real transport would appear to succeed while doing nothing.
Getting started
No uploadUrl, so nothing is sent: this is a picker with restrictions and previews. Drop files on the zone, paste a screenshot, or press Enter to browse.
No files selected
0 file(s) ready
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeFileUploader } from '@oge-ui/upload';
@Component({
selector: 'demo-root',
imports: [OgeFileUploader],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- With no uploadUrl this is a file picker: drag & drop, restrictions,
previews and removal, with everything chosen exposed through [(value)]
as plain File objects. Add uploadUrl and the same element uploads. -->
<oge-file-uploader
[(value)]="attachments"
accept="image/*,.pdf"
[maxFileSize]="5 * 1024 * 1024"
[maxFileCount]="5"
/>
<p>{{ attachments().length }} file(s) ready</p>
`,
})
export class Demo {
protected readonly attachments = signal<readonly File[]>([]);
}Uploading
uploadMode='useButtons' waits for the Upload button. Each row grows the affordances its state needs — progress, cancel, then retry. The request log shows exactly what would go over the wire.
No files selected
(no requests yet)
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeFileUploader } from '@oge-ui/upload';
@Component({
selector: 'demo-root',
imports: [OgeFileUploader],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- uploadMode decides when bytes move: 'instantly' on selection,
'useButtons' waits for the Upload button, 'select' never uploads.
Progress, cancel and retry appear on each row as the transfer needs them. -->
<oge-file-uploader
uploadUrl="/api/upload"
uploadMode="useButtons"
[uploadHeaders]="{ Authorization: 'Bearer demo' }"
[concurrency]="2"
(uploaded)="onUploaded($event.file.name, $event.response)"
(uploadFailed)="onFailed($event.file.name, $event.message)"
/>
`,
})
export class Demo {
protected onUploaded(name: string, response: unknown): void {
console.log('uploaded', name, response);
}
protected onFailed(name: string, message: string): void {
console.warn('failed', name, message);
}
}Chunked and resumable
One request per slice, carrying Kendo-shaped metadata. The scripted server fails chunk 2 once, so auto-retry is visible; Pause and Resume appear only here, because a whole-file request has nothing to suspend.
No files selected
(no requests yet)
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeFileUploader } from '@oge-ui/upload';
@Component({
selector: 'demo-root',
imports: [OgeFileUploader],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Chunked transfer sends one slice per request, carrying Kendo-shaped
metadata so an existing chunked endpoint needs no changes. Pause and
resume appear only here: a whole-file request has nothing to suspend,
so the affordance is derived from the transport rather than configured. -->
<oge-file-uploader
uploadUrl="/api/upload"
[chunk]="{ size: 512 * 1024, maxAutoRetries: 2, resumable: true }"
[autoRetry]="{ count: 2, delayMs: 400 }"
(chunkUploaded)="onChunk($event.chunkIndex, $event.totalChunks)"
/>
`,
})
export class Demo {
protected onChunk(index: number, total: number): void {
console.log(`chunk ${index + 1} of ${total}`);
}
}Restrictions
A rejected file stays on the list with its reason next to it. It remains in value — hiding it would let a required field pass while a file is plainly on screen — and the bound control is what goes invalid.
No files selected
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeFileUploader } from '@oge-ui/upload';
@Component({
selector: 'demo-root',
imports: [OgeFileUploader],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- A rejected file stays on the list with its reason next to it, rather
than vanishing. It is still in value — hiding it would let a required
field pass while the user can plainly see a file on screen — and the
bound form control is what goes invalid. -->
<oge-file-uploader
[allowedFileExtensions]="['.png', '.jpg', '.pdf']"
[maxFileSize]="1024 * 1024"
[minFileSize]="1024"
[maxTotalFileSize]="4 * 1024 * 1024"
[validateFile]="noSpaces"
(fileRejected)="lastRejection.set($event.errors[0].message)"
/>
@if (lastRejection(); as reason) {
<p role="status">{{ reason }}</p>
}
`,
})
export class Demo {
protected readonly lastRejection = signal<string | null>(null);
/** Returns a message to reject, or null to accept. */
protected readonly noSpaces = (file: File): string | null =>
file.name.includes(' ') ? 'File names must not contain spaces.' : null;
}Previews
Image rows render a thumbnail from an object URL the component owns and revokes on remove, clear and destroy.
No files selected
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeFileUploader } from '@oge-ui/upload';
@Component({
selector: 'demo-root',
imports: [OgeFileUploader],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- listType renders previews instead of a text row. The object URLs
behind them are owned by the component and revoked on remove, clear
and destroy — the first long-lived object URLs in the suite. -->
<oge-file-uploader
accept="image/*"
listType="pictureCard"
[previewWidth]="96"
/>
`,
})
export class Demo {}External drop zone
The drop surface is often a panel with nothing to do with the uploader's own markup — a different element, which no mode value can express.
No files selected
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeFileUploader, OgeUploadDropZone, OgeUploadTrigger } from '@oge-ui/upload';
@Component({
selector: 'demo-root',
imports: [OgeFileUploader, OgeUploadDropZone, OgeUploadTrigger],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The drop surface is often a whole panel that has nothing to do with
the uploader's own markup. That is a different element, which no mode
value can express — hence a directive, matched by name. -->
<div class="panel" [ogeUploadDropZone]="'attachments'">
Drop files anywhere in this panel
</div>
<button type="button" [ogeUploadTrigger]="'attachments'">Attach files…</button>
<oge-file-uploader dropZone="attachments" displayMode="compact" />
`,
})
export class Demo {}Angular forms
Reactive forms, Signal Forms and plain [(value)] all bind the same component. The restrictions reach the control as a plain ValidatorFn, so nothing is restated.
No files selected
valid: false
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeFileUploader } from '@oge-ui/upload';
import { ReactiveFormsModule, FormControl } from '@angular/forms';
@Component({
selector: 'demo-root',
imports: [OgeFileUploader, ReactiveFormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The restrictions are attached to the bound control as a plain
ValidatorFn, so the form goes invalid without restating maxFileSize as
a second rule that could drift. Signal Forms binds the same component
through [formField], and [(value)] works with no forms at all. -->
<oge-file-uploader
[formControl]="attachments"
[maxFileSize]="1024 * 1024"
[required]="true"
/>
<p>valid: {{ attachments.valid }}</p>
`,
})
export class Demo {
protected readonly attachments = new FormControl<readonly File[]>([], {
nonNullable: true,
});
}Templates
Six slots replace any part of the rendering. The row context arrives pre-formatted, so a custom row needs no size formatter of its own.
No files selected
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeFileUploader, OgeUploadFileTemplate } from '@oge-ui/upload';
@Component({
selector: 'demo-root',
imports: [OgeFileUploader, OgeUploadFileTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Six slots replace any part of the rendering. The row context arrives
pre-formatted, so a custom row needs no size formatter of its own. -->
<oge-file-uploader accept="image/*">
<ng-template ogeUploadFileTemplate let-file let-size="size" let-status="status">
<strong>{{ file.name }}</strong>
<span>{{ size }} · {{ status }}</span>
</ng-template>
</oge-file-uploader>
`,
})
export class Demo {}