pota/use/upload

pota/use/upload is an imperative file-upload primitive plus ref factories — the same upload pipeline (progress, optional content-addressed dedup, cancellation) regardless of how the files arrive. The uploadFile primitive does the work; upload wires it to a file <input>, and dropzone wires it to a drop target.

Exports

upload — ref factory for file inputs

upload(options) attaches to an <input type="file"> and uploads its selection on every change event. The input is cleared as soon as the selection is read, so re-selecting the same file fires change again — opt out with clearOnUpload: false. In-flight uploads abort when the surrounding reactive scope is disposed.

Arguments

upload(options) takes a single options object and returns a ref function (node: HTMLInputElement) => void.

Option Type Description
endpoint string URL the file is POSTed to as multipart/form-data.
field string Form field name for the file (default 'file').
existsUrl (hash, file) => string Optional content-addressed dedup: HEAD this URL first, skip upload on a 2xx.
parseResponse (text, xhr) => string Extract the result URL from the response body (default: trimmed body text).
accept string <input accept>-style filter; non-matching files fire onReject(file, 'type').
maxSize number Max file size in bytes; larger files fire onReject(file, 'size').
onProgress ({ file, loaded, total }) => void Fires during the POST; cached/HEAD hits get one event with loaded === total.
onUpload (results) => void Fires once at the end with the array of successful UploadResults; skipped when none succeeded. Required.
onFile (result) => void Fires per successfully uploaded file.
onError (error, file) => void Fires per file that failed (aborted uploads are silent).
onReject (file, reason) => void Fires per file rejected by accept/maxSize; reason is 'type' or 'size'.
clearOnUpload boolean Clear the input once the selection is read so the same file can be re-selected (default true).

Returns: a ref function (node: HTMLInputElement) => void for use:ref.

Filters: accept and maxSize

accept mirrors <input accept>: MIME types (image/png), wildcard MIME (image/*), extensions with leading dot (.pdf), comma- separated lists. maxSize is in bytes. Files that don't match fire onReject(file, 'type' | 'size') and never hit the network. These options apply to upload and dropzone alike.

Examples

Upload from a file input

Attaches upload to a multiple file input with type and size filters and the full set of lifecycle callbacks.

import { render, signal } from 'pota'
import { upload } from 'pota/use/upload'

function App() {
	const log = signal('')

	return (
		<div>
			<input
				type="file"
				multiple
				use:ref={upload({
					endpoint: '/api/upload',
					accept: 'image/*',
					maxSize: 5 * 1024 * 1024,
					onUpload: results =>
						log.write(`all done ${results.length}`),
					onFile: r => log.write(`done ${r.url}`),
					onError: (err, file) =>
						log.write(`${file.name}: ${err.message}`),
					onReject: (file, reason) =>
						log.write(`${file.name} rejected: ${reason}`),
				})}
			/>
			<p>{log.read}</p>
		</div>
	)
}

render(App)

Live progress per file

Tracks per-file progress in a signal-backed map and renders a bar for each upload. onProgress fires repeatedly during the POST.

import { render, signal } from 'pota'
import { For } from 'pota/components'
import { upload } from 'pota/use/upload'

function App() {
	const progress = signal<Record<string, number>>({})
	const status = signal('')

	return (
		<>
			<input
				type="file"
				multiple
				use:ref={upload({
					endpoint: '/api/upload',
					onProgress: ({ file, loaded, total }) =>
						progress.update(p => ({
							...p,
							[file.name]: loaded / total,
						})),
					onUpload: () => status.write('done'),
				})}
			/>
			<p>{status.read}</p>
			<For each={() => Object.entries(progress.read())}>
				{([name, ratio]) => (
					<div>
						{name}: {() => Math.round(ratio * 100)}%
					</div>
				)}
			</For>
		</>
	)
}

render(App)