Morphos & Kosmesis are here.Explore
PraxisJS

File Upload

@praxisjs/composables file upload composables — FileSelection, DropZone, FileUpload and FilePreview for picking, validating, previewing and uploading files.

File Upload

Composables for the file upload flow: pick or drop files, validate them, preview images and send them to your server with progress tracking. Attach them with @Compose.

npm install @praxisjs/composables
pnpm add @praxisjs/composables
yarn add @praxisjs/composables
bun add @praxisjs/composables

All composables share the same validation options:

OptionTypeDescription
acceptstring | string[]Same syntax as <input accept>: .pdf, image/png, image/*
maxSizenumberMaximum bytes per file
minSizenumberMinimum bytes per file
maxFilesnumberMaximum number of files held at once

Rejected files never throw — they land in errors as { file, code, message }, where code is 'type' | 'max-size' | 'min-size' | 'max-files'.


FileSelection

Opens the native file dialog and keeps the validated selection.

import { FileSelection } from '@praxisjs/composables'
import { Compose } from '@praxisjs/decorators'

@Component()
class Avatar extends StatefulComponent {
  @Compose(FileSelection, { accept: 'image/*', maxSize: 2 * 1024 * 1024 })
  picker!: FileSelection

  render() {
    return (
      <div>
        <button onClick={() => this.picker.open()}>Choose image</button>
        {() => this.picker.files.map((f) => <p>{f.name}</p>)}
        {() => this.picker.errors.map((e) => <p class="error">{e.message}</p>)}
      </div>
    )
  }
}

Options: validation options plus multiple (defaults to true when maxFiles > 1; otherwise a new selection replaces the previous one) and onFiles(files).

Properties: files: File[], errors: FileError[]

Methods: open(), add(files) (accepts a FileList — wire it to your own <input onChange>), remove(file), clear()


DropZone

Same as FileSelection, plus drag and drop on an element. Takes a ref as the first argument.

@Ref<HTMLDivElement>()
zoneRef!: Ref<HTMLDivElement>

@Compose(DropZone, 'zoneRef', { accept: ['.pdf'], maxFiles: 5 })
drop!: DropZone

render() {
  return (
    <div
      ref={this.zoneRef}
      style={() => `border:2px dashed ${this.drop.dragging ? '#6d5bbd' : '#ccc'}`}
      onClick={() => this.drop.open()}
    >
      Drop PDFs here or click to browse
    </div>
  )
}

Extra property: dragging: boolean — true while files are dragged over the zone (nested children don't make it flicker). Dragging anything other than files is ignored.


FileUpload

Uploads files with XMLHttpRequest so you get real upload progress. Files are queued, uploaded up to concurrency at a time, and each one is tracked as an UploadItem.

@Compose(FileUpload, {
  url: '/api/upload',
  headers: () => ({ Authorization: `Bearer ${token()}` }),
  accept: 'image/*',
  maxSize: 5 * 1024 * 1024,
  concurrency: 2,
})
upload!: FileUpload

render() {
  return (
    <div>
      <input type="file" multiple onChange={(e) => this.upload.add((e.target as HTMLInputElement).files)} />
      <p>{() => this.upload.progress}%</p>
      {() => this.upload.items.map((item) => (
        <div>
          {item.file.name} — {item.status} ({item.progress}%)
          {item.status === 'error' && <button onClick={() => this.upload.retry(item.id)}>Retry</button>}
          {item.status === 'uploading' && <button onClick={() => this.upload.cancel(item.id)}>Cancel</button>}
        </div>
      ))}
    </div>
  )
}

Combine it with a DropZone by passing its files on: onClick={() => this.upload.add(this.drop.files)}.

Options

OptionDefaultDescription
url—String, or (file) => string
method'POST'HTTP method
headers—Object, or function returning one (evaluated per upload)
fieldName'file'Form field that carries the file
data—Extra form fields — object, or (file) => object
withCredentialsfalseSend cookies cross-origin
concurrency3Simultaneous uploads
autoStarttrueUpload on add(); when false, call start()
onSuccess / onError—Called with the finished UploadItem

Plus the validation options.

Properties: items: UploadItem[], errors: FileError[], uploading: boolean, progress: number (0–100 average)

Methods: add(files), start(), cancel(id), retry(id), remove(id), clear()

An UploadItem is { id, file, status, progress, response?, error? } where status is 'pending' | 'uploading' | 'success' | 'error' | 'aborted'. response is the parsed JSON body (or raw text when it isn't JSON). In-flight requests are aborted when the component unmounts.


FilePreview

Turns a File into an object URL for <img>/<video> previews. The URL is revoked when the file changes and on unmount, so you never leak blob URLs.

@State() file: File | null = null

@Compose(FilePreview, getter('file'))
preview!: FilePreview

render() {
  return <img src={() => this.preview.url ?? ''} />
}

Properties: url: string | null


Helpers

import { formatFileSize, validateFiles } from '@praxisjs/composables'

formatFileSize(1536)               // "1.5 KB"
validateFiles(files, 0, { accept: '.png', maxSize: 1024 }) // { accepted, rejected }

On this page