For AI agents: the complete documentation index is available at https://a3s-lab.github.io/UI/v0.4.0/en/llms.txt, the full documentation bundle is available at https://a3s-lab.github.io/UI/v0.4.0/en/llms-full.txt, and this page is available as Markdown at https://a3s-lab.github.io/UI/v0.4.0/en/components/form-system/file-upload.md.

File upload

File upload is an official field extension that requires a host service. Renderer handles selection, drag and drop, preflight validation, the concurrent queue, progress, cancellation, retry after failure, the uploaded-file list, and removal state. The host handles authorization, network transfer, storage, scanning, download, and deletion.

Controlled values contain only closed JSON file references. Browser File objects, Blobs, temporary URLs, upload credentials, request state, and progress never enter the FormDocument, FormPlan, or form value.

实时示例FormFileReference[]
必填
已使用 1 / 3

支持 PDF 和常见图片,单个文件不超过 5 MB

拖放文件到这里或从设备中选择

.pdf,image/* · 单个不超过 5 MB · 最多 3 个

  • release-notes.pdf180 KB · 已上传
受控值
{
  "attachments": [
    {
      "id": "release-notes",
      "name": "release-notes.pdf",
      "size": 184320,
      "mediaType": "application/pdf"
    }
  ]
}

The example uses a local mock service to demonstrate progress and removal delay. It does not send or store file contents. Select a PDF or image to see the upload result appear immediately in the controlled value.

Value shape

Each file has four stable properties:

interface FormFileReference {
  id: string;
  name: string;
  size: number;
  mediaType: string;
}
PropertyTypeConstraintPurpose
idstring1–512 charactersStable file identifier in host storage
namestring1–1,024 charactersDisplay filename
sizeNon-negative safe integerBytesSize display and audit
mediaTypestring1–255 charactersMIME type

Reference objects reject additional properties. The host should resolve download URLs, signing parameters, access tokens, bucket names, and internal paths from id.

{
  "attachments": [
    {
      "id": "file_01JZ8K3Q2W",
      "name": "release-notes.pdf",
      "size": 184320,
      "mediaType": "application/pdf"
    }
  ]
}

Schema

createFormFileUploadSchema creates a closed array-of-objects Schema with minimum and maximum file counts.

import { createFormFileUploadSchema } from '@a3s-lab/ui/form/react';

const schema = {
  type: 'object',
  properties: {
    attachments: createFormFileUploadSchema({
      minFiles: 1,
      maxFiles: 5,
    }),
  },
  required: ['attachments'],
  additionalProperties: false,
};
ParameterTypeDefaultConstraint
minFilesnumber0Safe integer from 0 through maxFiles
maxFilesnumber5Safe integer from 0 through 100

required means the array property must exist; minFiles specifies how many files that array must contain. These constraints serve different purposes.

UI node properties

const node = {
  id: 'attachments',
  kind: 'field',
  label: 'Attachments',
  description: 'Upload the files required for review',
  schemaPath: '/properties/attachments',
  widget: 'a3s.file-upload',
  customProps: {
    accept: '.pdf,image/*',
    maxFileSize: 10 * 1024 * 1024,
    maxConcurrentUploads: 2,
  },
};
PropertyTypeDefaultRuntime behavior
customProps.acceptstringEmptyComma-separated extensions, complete MIME types, or MIME wildcards such as image/*
customProps.maxFileSizenumber10 MiBPer-file byte limit from 1 B through 5 GiB
customProps.maxConcurrentUploadsnumber2Concurrent uploads for this field, from 1 through 4
Schema minItemsnumber0Minimum file count required for submission validation
Schema maxItemsnumber5Total capacity for uploaded files and retryable tasks

Designer's properties panel edits accepted file types, the per-file limit, and concurrency. Its validation panel edits minimum and maximum file counts. The design canvas shows a summary of accepted types, size, and count without performing a real upload.

Host file service

createFileUploadNodeRegistry accepts a typed host service. Core and Renderer do not call a storage SDK directly.

import {
  createFileUploadNodeRegistry,
  type FormFileService,
} from '@a3s-lab/ui/form/react';

const fileService: FormFileService = {
  async upload(request) {
    const stored = await storage.upload(request.file, {
      signal: request.signal,
      onProgress: request.onProgress,
    });

    return {
      id: stored.id,
      name: request.file.name,
      size: request.file.size,
      mediaType: request.file.type || 'application/octet-stream',
    };
  },

  async remove(request) {
    await storage.remove(request.file.id, { signal: request.signal });
  },

  async open(request) {
    await storage.open(request.file.id, { signal: request.signal });
  },
};

const nodeRegistry = createFileUploadNodeRegistry({ service: fileService });

All three requests include the same field context:

PropertyTypeDescription
nodeCompiledNodeCurrent file field and its configuration
planFormPlanCurrent immutable compiled result
valuePathstring | undefinedCurrent concrete value path
rowIndicesreadonly number[]Concrete row indices inside nested repeaters
signalAbortSignalCancellation, unmount, and task-termination signal

upload additionally receives the browser file and onProgress({ loaded, total? }). remove and optional open receive an already validated FormFileReference.

Service failures show a generic message without internal exceptions by default. To expose host error codes, map them to public copy with getErrorMessage:

const nodeRegistry = createFileUploadNodeRegistry({
  service: fileService,
  getErrorMessage(error, operation) {
    if (isQuotaError(error)) return 'Storage quota exceeded. Free space and retry.';
    if (operation === 'remove') return 'The file cannot be removed yet. Retry later.';
    return undefined;
  },
});

Mapped messages display at most 500 characters. Do not return raw exceptions, response bodies, or credentials directly to the interface.

React

Custom component capabilities must enter both the compiler and Renderer. Designer adds component capabilities automatically from nodeRegistry; pass registration keys explicitly when compiling separately.

import { assertCompiled } from '@a3s-lab/ui/form/core';
import {
  createFileUploadNodeRegistry,
  FormRenderer,
} from '@a3s-lab/ui/form/react';
import '@a3s-lab/ui/form/a3s-ui.css';

const nodeRegistry = createFileUploadNodeRegistry({ service: fileService });
const plan = assertCompiled(document, {
  capabilities: { widgets: Object.keys(nodeRegistry) },
});

<FormRenderer
  plan={plan}
  value={value}
  onChange={setValue}
  nodeRegistry={nodeRegistry}
/>;

Vue

The Vue adapter forwards the same registry and controlled value, so it does not need another upload implementation.

<script setup lang="ts">
import { ref } from 'vue';
import { A3SFormRenderer } from '@a3s-lab/ui/form/vue';
import { createFileUploadNodeRegistry } from '@a3s-lab/ui/form/react';
import '@a3s-lab/ui/form/a3s-ui.css';

const value = ref({ attachments: [] });
const nodeRegistry = createFileUploadNodeRegistry({ service: fileService });
</script>

<template>
  <A3SFormRenderer
    :plan="plan"
    v-model="value"
    :node-registry="nodeRegistry"
  />
</template>

Web Component

Pass the registry as an element property. Value changes continue to use the value-change event.

import { defineA3SFormElements } from '@a3s-lab/ui/form/web-component';
import { createFileUploadNodeRegistry } from '@a3s-lab/ui/form/react';

defineA3SFormElements();

const renderer = document.querySelector('a3s-form-renderer');
renderer.plan = plan;
renderer.value = { attachments: [] };
renderer.nodeRegistry = createFileUploadNodeRegistry({ service: fileService });
renderer.addEventListener('value-change', (event) => {
  saveDraft(event.detail);
});

Interaction states

StateBehavior
EmptyShow the drop zone, select button, and accepted-type summary
QueuedFiles beyond the concurrency limit wait for a free slot
UploadingShow filename, size, native progress semantics, and a cancel action
Upload failedRetain the local File for retry and allow removal of the failed item
UploadedShow the stable reference and allow host-provided open and remove capabilities
RemovingLock actions for this file and update the controlled value only after host success
Removal failedLeave the file reference unchanged and allow retrying removal
Read-only or disabledPrevent selection, drop, cancellation, and removal; keep the host-provided open action available

File type, size, and capacity are checked before any network request. At most 120 local tasks are retained at once to prevent abnormal selections from creating unbounded runtime state. Unmounting the component aborts any upload, removal, or open requests still in progress.

Keyboard and assistive technology

  • File selection uses native input[type="file"], which can receive focus with Tab and opens the system file picker through the browser.
  • Cancel, retry, open, and remove actions use button names containing the complete filename.
  • Upload progress uses native progress semantics.
  • Successful uploads and removals are announced through aria-live="polite".
  • Errors use role="alert"; Schema errors continue to appear in the form error summary.
  • Long filenames are truncated visually, while the full name remains in the title and accessible name.
  • In a 390px-wide container, the select button and file actions become a reachable vertical layout.
  • prefers-reduced-motion disables nonessential transitions.

Security boundary

  • Authorization checks must occur in the host service, not rely solely on the file picker's accept value.
  • MIME type, extension, and file content must be revalidated by a trusted service.
  • Virus scanning, content moderation, quotas, retention policy, and audit are the host's responsibility.
  • An id should be unguessable or resolved with authorization.
  • open should acquire short-lived access on demand instead of writing a signed URL to the form value.
  • After a successful upload, only the four declared properties are accepted; a return value with extra fields enters the failed state.
  • Cancellation signals must reach the actual network request so an unmounted interface does not continue consuming bandwidth.
  • Do not remove a reference from the controlled value before deletion succeeds, which would let interface state diverge from storage state.