react-formbridge
Browse documentation
Field buildersv1.0.2

field.file()

File upload builder for documents, media, and attachments. This is the only builder that does not extend BaseFieldBuilder.

  • Has its own upload-focused fluent API: accept(), maxSize(), multiple(), preview(), source(), resize()
  • render(), transform(), and the conditional helpers from BaseFieldBuilder are not available on this builder
  • Platform differences are handled by the renderer - the schema keeps the business contract
import { useState } from 'react'
import { field, useFormBridge, type FileValue } from '@runilib/react-formbridge'

const schema = {
  avatar: field.file('Avatar')
    .accept(['image/jpeg', 'image/png', 'image/webp'])
    .maxSize(5 * 1024 * 1024)
    .preview(140)
    .required('Please upload a photo.'),
  attachments: field.file('Attachments')
    .multiple(3)
    .accept(['image/png', 'image/jpeg', 'application/pdf'])
    .maxSize(2 * 1024 * 1024)
    .dragLabel('Drop up to 3 files here (PNG, JPG, PDF · 2 MB each)'),
}

function formatSize(bytes: number): string {
  if (bytes < 1024) return bytes + ' B'
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
  return (bytes / (1024 * 1024)).toFixed(2) + ' MB'
}

function summarize(value: FileValue | FileValue[] | null): string {
  if (!value) return 'empty'
  const list = Array.isArray(value) ? value : [value]
  if (list.length === 0) return 'empty'
  return list
    .map((file) => `${file.name} · ${file.type} · ${formatSize(file.size)}`)
    .join('\n')
}

export function FilePlaygroundWeb() {
  const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(null)
  const { Form, fields, state } = useFormBridge(schema, {
    validateOn: 'onBlur',
    revalidateOn: 'onChange',
  })

  return (
    <div
      style={{
        fontFamily: 'sans-serif',
        padding: 20,
        background: '#f5f7fb',
        display: 'grid',
        gap: 16,
      }}
    >
      <div>
        <h3 style={{ margin: '0 0 8px' }}>Interactive file uploads</h3>
        <p style={{ margin: 0, color: '#4b5563' }}>
          Pick an avatar and a few attachments, then submit to inspect the
          platform-agnostic <code>FileValue</code> payload FormBridge exposes.
        </p>
      </div>

      <Form onSubmit={async (values) => setSubmitted(values)}>
        <div style={{ display: 'grid', gap: 16 }}>
          <fields.avatar />
          <fields.attachments />
          <Form.Submit disabled={!state.isValid}>Upload</Form.Submit>
        </div>
      </Form>

      <div style={{ display: 'grid', gap: 12 }}>
        <div
          style={{
            border: '1px solid #d6d9e0',
            borderRadius: 12,
            padding: 12,
            background: '#fff',
          }}
        >
          <strong>Live summary</strong>
          <pre style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
{`avatar:
${summarize(state.values.avatar)}

attachments:
${summarize(state.values.attachments)}`}
          </pre>
        </div>

        <div
          style={{
            border: '1px solid #d6d9e0',
            borderRadius: 12,
            padding: 12,
            background: '#fff',
          }}
        >
          <strong>Last submit (raw FileValue payload)</strong>
          <pre style={{ marginBottom: 0, whiteSpace: 'pre-wrap' }}>
            {JSON.stringify(
              submitted,
              (key, value) =>
                key === 'base64' && typeof value === 'string'
                  ? `<${value.length} chars>`
                  : value,
              2,
            )}
          </pre>
        </div>
      </div>
    </div>
  )
}

export default FilePlaygroundWeb

Defaults, inheritance & field methods

This builder has its own upload-focused fluent API rather than the full BaseFieldBuilder surface.

Defaults:

  • defaultValue is null for single-file mode, or [] after calling multiple()
  • accepted types default to any file type
  • drag & drop is enabled on web by default
  • file previews are off by default

File-builder methods:

MethodTypeDescription
label(text)text: stringSets the field label rendered above the uploader.
required(message?)message?: stringMarks file selection as mandatory. Default message: "Please select a file.".
hint(text)text: stringAdds helper copy below the uploader.
disabled(value = true)value?: booleanDisables or re-enables the uploader.
hidden(value = true)value?: booleanHides or shows the uploader.
accept(types)types: string[]Restricts accepted MIME types. Supports wildcards like image/*. Validated client-side against the actual file type.
maxSize(bytes)bytes: numberMaximum size per file in bytes. Validation runs against every file when multiple() is enabled and rejects with a human-readable MB message.
multiple(max = 10)max?: numberEnables multi-file mode, sets the maximum allowed file count, and switches the default value from null to [].
preview(height = 160)height?: numberEnables image previews and sets their preview height in px.
source(type)'gallery' | 'camera' | 'documents' | 'all'Native-only: which picker to open. all (default) shows an action sheet so the user can choose. Web ignores this since the browser file dialog handles all sources.
withBase64()() => thisRequests base64 encoding alongside the file payload - needed for upload APIs that expect inline payloads instead of multipart streams.
resize(maxWidth, maxHeight, quality = 0.9)number, number, number?Native-only: auto-resize images before they enter the form value. Useful to keep mobile uploads under the size limit before they hit the network.
allowVideo()() => thisNative-only: includes video files in the gallery/camera picker. Web relies on accept() instead.
noDragDrop()() => thisWeb-only: disables the drop-zone surface and shows just the browse button.
dragLabel(label)label: stringWeb-only: customizes the drop-zone copy. Defaults to "Drag & drop a file here, or click to browse".
visibleWhen(field, value?) / visibleWhenNot / visibleWhenTruthy / visibleWhenFalsy / visibleWhenAnyfield | predicateConditional visibility rules. Same shape as the BaseFieldBuilder helpers - show the uploader only when another field matches a value, is truthy/falsy, or matches any of several pairs.
requiredWhen(field, value?) / requiredWhenAnyfield | predicateConditional required rules so the uploader only becomes mandatory when another field hits a given state.
disabledWhen(field, value?)field | predicateConditional disabled rule so the uploader locks based on another field.
resetOnHide() / keepOnHide() / clearOnHide()() => thisControls what happens to the selected file(s) when the field becomes hidden by a visibility rule.

Static presets - shortcuts that pre-configure common upload flows:

MethodTypeDescription
FileFieldBuilder.profilePhoto(label?)label?: stringProfile photo preset: JPG/PNG/WebP, 5 MB cap, gallery source, preview enabled, image resized to 1024×1024 at 0.85 quality.
FileFieldBuilder.document(label?)label?: stringPDF-only document preset, 10 MB cap, document picker source.
FileFieldBuilder.attachments(label?, max = 5)label?: string, max?: numberMulti-attachment preset accepting images and PDFs, 10 MB cap each, preview enabled, drag zone hint adapts to the max.
FileFieldBuilder.spreadsheet(label?)label?: stringCSV/XLS/XLSX import preset, 20 MB cap, document picker source.

Base-builder relationship:

  • See Base field builder for the shared surface most other builders inherit
  • field.file() ships its own conditional helpers (visibleWhen, requiredWhen, disabledWhen, resetOnHide, …) that mirror the BaseFieldBuilder shape, so it integrates with the same conditional logic as every other field
  • It does not expose render(), transform(), or the value-coercion helpers because it does not extend the full BaseFieldBuilder surface

Recipes

Patterns that showcase file-specific strengths.

Avatar with preview

Avatar.tsxtsx
1const schema = {
2 avatar: field.file('Avatar')
3 .accept(['image/jpeg', 'image/png'])
4 .maxSize(5 * 1024 * 1024)
5 .preview(120)
6 .required('Please upload a photo.'),
7}

Mobile document capture

IdentityCard.tsxtsx
1const schema = {
2 identityCard: field.file('Identity card')
3 .accept(['image/jpeg', 'image/png', 'application/pdf'])
4 .source('documents')
5 .withBase64()
6 .maxSize(10 * 1024 * 1024),
7}

Multi-attachment with drag zone

Attachments.tsxtsx
1const schema = {
2 attachments: field.file('Attachments')
3 .multiple(5)
4 .accept(['application/pdf', 'image/png'])
5 .dragLabel('Drop files here or click to browse'),
6}

Optimized product media

ProductMedia.tsxtsx
1const schema = {
2 productPhotos: field.file('Product photos')
3 .accept(['image/jpeg', 'image/png', 'video/mp4'])
4 .resize(1600, 1600, 0.85)
5 .allowVideo()
6 .multiple(10),
7}

Click-only (no drag)

DocumentsOnly.tsxtsx
1const schema = {
2 documents: field.file('Documents')
3 .accept(['application/pdf'])
4 .noDragDrop(),
5}