Upload fields

Two families: auto fields upload immediately and store what the API returns; the plain dropzone keeps the File for you to send. This playground uploads to a mock endpoint unless NUXT_PUBLIC_UPLOAD_API is set.

Source in the kit: components/Form/InputUpload*/ · helpers/uploadCore.ts

What the upload API must return

The auto fields POST multipart/form-data with the file under file ( bodyKey ) and read these keys from the JSON answer. Each key can be renamed, or be a path such as data.url , with responseURL , responsePath , responseName , responseSize , responseID .

{
  "url": "https://cdn.example.com/files/contract.pdf",
  "path": "/files/contract.pdf",
  "name": "contract.pdf",
  "size": 48213,
  "id": "f_01HZX"
}

One file, uploaded on pick

examples/fields/Upload.vue

คลิกเพื่อเลือกไฟล์

หรือ ลากและวางที่นี่

PDF or image, up to 5 MB

<template>
  <FormFields :options="fields" />
</template>

<script lang="ts" setup>
const form = useForm()

// Where uploads go: baseURL + headers. In an app this usually adds the Authorization header.
const requestOptions = useRequestOptions().getFile()

const fields = createFormFields(() => [
  {
    type: INPUT_TYPES.UPLOAD_DROPZONE_AUTO,
    props: {
      name: 'contract',
      label: 'UPLOAD_DROPZONE_AUTO',
      placeholder: 'PDF or image, up to 5 MB',
      requestOptions,
      uploadPathURL: '/uploads',
      // Extensions, MIME types or wildcards
      accept: '.pdf,image/*',
      // In KB
      maxSize: 5120,
    },
    on: {
      success: (file: IFileValue) => console.log('uploaded', file),
    },
  },
])

// The file is uploaded as soon as it is picked; the form value is the API's answer, not the File:
// { url, path, name, size, id }
defineExpose({
  debug: computed(() => form.values),
})
</script>

composables/useRequestOptions.ts

import type { AxiosRequestConfig } from 'axios'

export const useRequestOptions = () => {
  const config = useRuntimeConfig()

  // The mock API is served by this same app, so its address is wherever the page came from.
  // A fixed http://localhost:3000 only works on the machine running the dev server.
  const origin = useRequestURL().origin

  const getMock = (): Omit<AxiosRequestConfig, 'baseURL'> & {
    baseURL: string
  } => {
    return {
      baseURL: String(config.public.baseAPIMock || `${origin}/api/mock`),
    }
  }

  const getFile = (): Omit<AxiosRequestConfig, 'baseURL'> & {
    baseURL: string
  } => {
    // Set NUXT_PUBLIC_UPLOAD_API and NUXT_PUBLIC_UPLOAD_TOKEN to upload to a real API; falls back to the local mock
    const uploadAPI = config.public.uploadAPI as string
    const uploadToken = config.public.uploadToken as string

    if (!uploadAPI) return getMock()

    return {
      baseURL: uploadAPI,
      headers: uploadToken
        ? {
            Authorization: `Bearer ${uploadToken}`,
          }
        : {},
    }
  }

  const getDefault = (): Omit<AxiosRequestConfig, 'baseURL'> & {
    baseURL: string
  } => {
    return {
      baseURL: String(config.public.baseAPI),
    }
  }

  return {
    getDefault,
    getMock,
    getFile,
  }
}

Several files

examples/fields/UploadMultiple.vue

คลิกเพื่อเลือกไฟล์

หรือ ลากและวางที่นี่

Up to 3 files, 5 MB each

existing-photo.png

20.48 KB

<template>
  <FormFields :options="fields" />
</template>

<script lang="ts" setup>
const form = useForm({
  initialValues: {
    // Existing files (when editing a record) are the same shape the API returned
    attachments: [
      {
        url: 'https://placehold.co/600x400.png',
        name: 'existing-photo.png',
        size: 20480,
      },
    ],
  },
})

const requestOptions = useRequestOptions().getFile()

const fields = createFormFields(() => [
  {
    type: INPUT_TYPES.UPLOAD_DROPZONE_AUTO_MULTIPLE,
    props: {
      name: 'attachments',
      label: 'UPLOAD_DROPZONE_AUTO_MULTIPLE',
      placeholder: 'Up to 3 files, 5 MB each',
      requestOptions,
      uploadPathURL: '/uploads',
      maxSize: 5120,
      maxFiles: 3,
    },
  },
])

// Files upload one after another; each finished file is added to the front of the array.
// A failed file stays in the list with a retry button and does not stop the others.
defineExpose({
  debug: computed(() => form.values),
})
</script>

Images, with dimension rules

examples/fields/UploadImage.vue

Any image up to 2 MB

คลิกเพื่อเลือกรูปภาพ

หรือ ลากและวางที่นี่

At least 800px wide, 16:9

คลิกเพื่อเลือกรูปภาพ

หรือ ลากและวางที่นี่

<template>
  <FormFields
    :options="fields"
    class="grid gap-x-4 md:grid-cols-2"
  />
</template>

<script lang="ts" setup>
const form = useForm()
const requestOptions = useRequestOptions().getFile()

const fields = createFormFields(() => [
  {
    type: INPUT_TYPES.UPLOAD_IMAGE_AUTO,
    props: {
      name: 'avatar',
      label: 'UPLOAD_IMAGE_AUTO',
      description: 'Any image up to 2 MB',
      requestOptions,
      uploadPathURL: '/uploads',
      maxSize: 2048,
    },
  },
  {
    type: INPUT_TYPES.UPLOAD_IMAGE_AUTO,
    props: {
      name: 'banner',
      label: 'With dimension rules',
      description: 'At least 800px wide, 16:9',
      requestOptions,
      uploadPathURL: '/uploads',
      // Checked in the browser before anything is uploaded:
      // width, height, minWidth, minHeight, maxWidth, maxHeight, aspectRatio, aspectRatioTolerance
      dimensions: {
        minWidth: 800,
        aspectRatio: 16 / 9,
        aspectRatioTolerance: 0.05,
      },
    },
  },
])

defineExpose({
  debug: computed(() => form.values),
})
</script>

Keep the File and send it yourself

examples/fields/UploadFile.vue

คลิกเพื่อเลือกไฟล์

หรือ ลากและวางที่นี่

<template>
  <Form @submit="onSubmit">
    <FormFields :options="fields" />

    <Button
      type="submit"
      class="mt-4"
    >
      Build FormData
    </Button>
  </Form>
</template>

<script lang="ts" setup>
const form = useForm()

// UPLOAD_DROPZONE uploads nothing: the form value is the File itself.
// Use it when the file must travel with the rest of the form in one multipart request.
const fields = createFormFields(() => [
  {
    type: INPUT_TYPES.TEXT,
    props: {
      name: 'title',
      label: 'Title',
    },
  },
  {
    type: INPUT_TYPES.UPLOAD_DROPZONE,
    props: {
      name: 'document',
      label: 'UPLOAD_DROPZONE',
      accept: '.pdf',
      maxSize: 5120,
    },
  },
])

const sent = ref<string[]>([])

const onSubmit = form.handleSubmit((values) => {
  const body = new FormData()

  body.append('title', values.title ?? '')

  if (values.document) body.append('document', values.document)

  // loader.run({ data: body }) would send it; here we only list what is inside
  sent.value = Array.from(body.entries()).map(
    ([key, value]) => `${key}: ${value instanceof File ? `File(${value.name})` : value}`
  )
})

defineExpose({
  debug: computed(() => ({
    values: form.values,
    formData: sent.value,
  })),
})
</script>