react-formbridge
Browse documentation
Field buildersv2.0.0

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
  • Your application renders the browser input, drop zone, or native picker; 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')
}

function toFileValue(file: File): FileValue {
  return {
    uri: URL.createObjectURL(file),
    name: file.name,
    type: file.type || 'application/octet-stream',
    size: file.size,
  }
}

function FileField({
  form,
  name,
  accept,
  multiple = false,
  preview = false,
}) {
  const file = form.fieldController(name)
  const selected = Array.isArray(file.value)
    ? file.value
    : file.value
      ? [file.value]
      : []

  if (!file.visible) return null

  return (
    <label
      style={{
        display: 'grid',
        gap: 8,
        padding: 16,
        border: `1px dashed ${file.error ? '#dc2626' : '#9ca3af'}`,
        borderRadius: 12,
        background: '#fff',
      }}
    >
      <strong>{file.label}{file.required ? ' *' : ''}</strong>
      <span>Drop files here or choose them from your device.</span>
      <input
        type="file"
        accept={accept}
        multiple={multiple}
        disabled={file.disabled}
        onChange={(event) => {
          const values = Array.from(event.target.files ?? []).map(toFileValue)
          file.onChange(multiple ? values : (values[0] ?? null))
        }}
        onBlur={file.onBlur}
      />
      {preview
        ? selected.map((value) =>
            value.type.startsWith('image/') ? (
              <img
                key={value.uri}
                src={value.uri}
                alt={value.name}
                style={{ width: 96, height: 96, objectFit: 'cover', borderRadius: 8 }}
              />
            ) : null,
          )
        : null}
      {selected.map((value) => (
        <span key={value.uri}>{value.name} · {formatSize(value.size)}</span>
      ))}
      {file.error ? <span role="alert" style={{ color: '#dc2626' }}>{file.error}</span> : null}
    </label>
  )
}

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

  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 }}>
          <FileField
            form={form}
            name="avatar"
            accept="image/jpeg,image/png,image/webp"
            preview
          />
          <FileField
            form={form}
            name="attachments"
            accept="image/png,image/jpeg,application/pdf"
            multiple
          />
          <button type="submit" disabled={!state.isValid}>Upload</button>
        </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 label exposed to your application-owned 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 picker intent. Use the same value when wiring your application-owned picker; web file dialogs ignore it.
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

Render a picker or drop zone

FormBridge does not open a picker or render an upload surface. Build that UI in your application and bind it with form.fieldController(name).

  • Web: map the browser FileList to FileValue objects, then call controller.onChange()
  • Native: map the result from Expo Document Picker, Expo Image Picker, or your preferred picker to FileValue
  • Send one FileValue | null in single mode, or a FileValue[] after multiple()
  • The application owns previews and temporary object-URL cleanup; FormBridge owns form state and schema validation

The interactive examples above deliberately pass accept, multiple, and picker behavior to the application-owned component so the UI visibly matches the schema configuration.

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}