Skip to content

feat(FileUpload): new component #4102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 40 commits into
base: v3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
cfc3810
wip
rdjanuar Nov 14, 2024
062a502
wip
rdjanuar Dec 1, 2024
643c1e0
up
rdjanuar Dec 1, 2024
32fdfb9
docs(showcase): fix contrast on light mode
vachmara Apr 4, 2025
07da4b8
Merge branch 'feat/file-upload' of https://github.com/rdjanuar/ui int…
vachmara Apr 20, 2025
e284dc8
Merge branch 'nuxt:v3' into v3
vachmara May 6, 2025
f4ab1be
Merge branch 'v3' of https://github.com/nuxt/ui into v3
vachmara May 6, 2025
3542bed
feat: add FileUpload minimal component
vachmara May 7, 2025
686843c
fix: use avatar for image preview and icon
vachmara May 7, 2025
0a5b982
Merge branch 'v3' of https://github.com/nuxt/ui into feat-file-upload
vachmara May 7, 2025
6afc872
Merge branch 'v3' of https://github.com/nuxt/ui into feat-file-upload
vachmara May 10, 2025
d466b87
Merge branch 'v3' into feat-file-upload
vachmara May 14, 2025
9797bb7
Merge branch 'feat-file-upload' of github.com:vachmara/ui into feat-f…
vachmara May 14, 2025
5daa768
refactor(FileUpload): cursor-not-allowed when disabled
vachmara May 14, 2025
c0545fb
feat(FileUpload): emits drag & drop events
vachmara May 14, 2025
b0c0e2b
test(FileUpload): create tests
vachmara May 15, 2025
cd42f9a
Merge branch 'v3' into feat-file-upload
vachmara May 15, 2025
f6d139d
docs(FileUpload): minimal doc
vachmara May 15, 2025
83ab576
Merge branch 'v3' into feat-file-upload
vachmara May 21, 2025
a6ced1e
fix: dragging ui depends also on disabled
vachmara May 21, 2025
98ae6cf
fix: avoid firing dragleave event because of nested elements
vachmara May 21, 2025
d2f1776
test: update FileUpload snapshot
vachmara May 21, 2025
2a28a11
test: udpate vue snapshot
vachmara May 21, 2025
43a3311
feat: add label and default translations
vachmara May 21, 2025
a5ffe51
chore(deps): update nuxt framework to ^3.17.4 (v3) (#4197)
renovate[bot] May 22, 2025
de9e00f
Merge branch 'v3' into feat-file-upload
vachmara May 22, 2025
a50ca6b
feat: custom file upload item generic type
vachmara May 22, 2025
7e5f6fc
test: update file upload type in tests
vachmara May 22, 2025
ac46e23
refactor: rename file slot to item
vachmara May 22, 2025
44603c9
test: item slot with custom type
vachmara May 22, 2025
42bd5ae
Merge branch 'v3' into feat-file-upload
vachmara May 22, 2025
39be42b
Merge branch 'v3' into feat-file-upload
vachmara May 22, 2025
6ca00f7
chore: ky locale
vachmara May 22, 2025
1fd6af9
Merge branch 'v3' of github.com:vachmara/ui into feat-file-upload
vachmara May 22, 2025
71d00e9
Merge branch 'v3' into feat-file-upload
vachmara May 22, 2025
bd71479
test: fix
vachmara May 23, 2025
9589ce4
Merge branch 'v3' of https://github.com/nuxt/ui into feat-file-upload
vachmara May 24, 2025
6577626
feat: improve documentation
vachmara May 24, 2025
bc1c1e0
docs: add examples
vachmara May 24, 2025
07e0845
test: fix vue tests
vachmara May 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts" setup>
import type { FileUploadItem } from '@nuxt/ui'

type UploadWithStatus = FileUploadItem<{ status: 'pending' | 'uploading' | 'done', progress?: number }>

const value = ref<UploadWithStatus[]>([{
file: new File([''], 'test.txt'),
status: 'pending'
}])
</script>

<template>
<UFileUpload v-model="value" />
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<script lang="ts" setup>
import type { FileUploadItem } from '@nuxt/ui'

const value = ref<FileUploadItem[]>([])
const requirements = [
{
test: (file: File) => /^image\//.test(file.type),
text: 'File must be an image.'
},
{
test: (file: File) => file.name.length <= 10,
text: 'File name must be less than 10 characters.'
},
{
test: (file: File) => !/\s/.test(file.name),
text: 'File name must not contain spaces.'
},
{
test: (file: File) => /\.(?:jpg|jpeg|png)$/i.test(file.name),
text: 'File name must end with .jpg, .jpeg, or .png.'
},
{
test: (file: File) => file.size < 1024 * 1024,
text: 'File size must be less than 1MB.'
}
]

const file = computed(() => value.value[0]?.file)
const checks = computed(() => file.value ? requirements.map(req => ({ met: req.test(file.value as File), text: req.text })) : requirements.map(req => ({ met: false, text: req.text })))
const score = computed(() => checks.value.filter(r => r.met).length)
const color = computed(() => {
if (!file.value) return 'neutral'
if (score.value === 0) return 'neutral'
if (score.value <= 2) return 'error'
if (score.value === 3 || score.value === 4) return 'warning'
return 'success'
})
const text = computed(() => {
if (!file.value) return 'Select a file'
if (score.value <= 2) return 'File does not meet requirements'
if (score.value === 3 || score.value === 4) return 'Almost valid file'
return 'File is valid!'
})
</script>

<template>
<div class="space-y-2">
<UFormField label="Upload your profile picture" required>
<UFileUpload
v-model="value"
accept="image/*"
upload-icon="i-lucide-upload"
file-icon="i-lucide-file"
required
/>
</UFormField>

<UProgress
:color="color"
:indicator="text"
:model-value="score"
:max="requirements.length"
size="sm"
/>

<p class="text-sm font-medium">
{{ text }}. Must satisfy:
</p>
<ul class="space-y-1" aria-label="File requirements">
<li
v-for="(req, index) in checks"
:key="index"
class="flex items-center gap-0.5"
:class="req.met ? 'text-success' : 'text-muted'"
>
<UIcon :name="req.met ? 'i-lucide-circle-check' : 'i-lucide-circle-x'" class="size-4 shrink-0" />
<span class="text-xs font-light">
{{ req.text }}
<span class="sr-only">
{{ req.met ? ' - Requirement met' : ' - Requirement not met' }}
</span>
</span>
</li>
</ul>
</div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script lang="ts" setup>
const value = ref([])
</script>

<template>
<UFormField
label="Upload your profile picture"
help="We won't share your profile picture."
required
>
<UFileUpload
v-model="value"
accept="image/png"
upload-icon="i-lucide-upload"
file-icon="i-lucide-file"
required
/>
</UFormField>
</template>
248 changes: 248 additions & 0 deletions docs/content/3.components/file-upload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
---
title: FileUpload
description: A drag-and-drop file upload component.
category: form
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/FileUpload.vue
navigation.badge: New
---

## Usage

Use the `v-model` directive to control the value of the Input.

::component-code
---
ignore:
- modelValue
external:
- modelValue
externalTypes:
- FileUploadItem[]
props:
modelValue: []
---
::

### Accept

Use the `accept` prop to specify the types of files that can be uploaded.

::component-code
---
ignore:
- modelValue
external:
- modelValue
externalTypes:
- FileUploadItem[]
props:
modelValue: []
accept: 'image/*'
---
::

### Label

Use the `label` prop to set the label of the component.

::component-code
---
ignore:
- modelValue
external:
- modelValue
externalTypes:
- FileUploadItem[]
props:
modelValue: []
label: 'Upload your image'
---
::

### Upload Icon

Use the `uploadIcon` prop to set a custom upload icon.

::component-code
---
ignore:
- modelValue
external:
- modelValue
externalTypes:
- FileUploadItem[]
props:
modelValue: []
uploadIcon: 'i-heroicons-cloud-arrow-up-solid'
---
::

::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.upload` key.
:::

#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.upload` key.
:::
::

### Size

Use the `size` prop to set the size of the component.

::component-code
---
ignore:
- modelValue
external:
- modelValue
externalTypes:
- FileUploadItem[]
props:
modelValue: []
size: xl
---
::

### Multiple

Use the `multiple` prop to allow multiple file uploads.

::component-code
---
ignore:
- modelValue
- multiple
external:
- modelValue
externalTypes:
- FileUploadItem[]
props:
modelValue: []
multiple: true
---
::

### File Icon

Use the `fileIcon` prop to set a custom file icon.

::component-code
---
ignore:
- modelValue
- accept
external:
- modelValue
externalTypes:
- FileUploadItem[]
props:
modelValue:
- file:
name: 'example.txt'
size: 3145728
type: 'text/plain'
fileIcon: 'i-heroicons-document-text-solid'
accept: 'text/plain'
---
::

::framework-only
#nuxt
:::tip{to="/getting-started/icons/nuxt#theme"}
You can customize this icon globally in your `app.config.ts` under `ui.icons.file` key.
:::

#vue
:::tip{to="/getting-started/icons/vue#theme"}
You can customize this icon globally in your `vite.config.ts` under `ui.icons.file` key.
:::
::

### Disabled

Use the `disabled` prop to disable the component.

::component-code
---
ignore:
- modelValue
external:
- modelValue
externalTypes:
- FileUploadItem[]
props:
modelValue: []
disabled: true
---
::

## Examples

### With custom type

You can extend the type to include additional properties.

::component-example
---
name: 'file-upload-extend-type-example'
---
::

### Within a FormField

You can use the FileUpload within a [FormField](/components/form-field) component to display a label, help text, required indicator, etc.

::component-example
---
name: 'file-upload-form-field-example'
---
::

::tip{to="/components/form"}
It also provides validation and error handling when used within a **Form** component.
::

### With file validation

You can build a custom validation function to check the file type and size.

::component-example
---
collapse: true
name: 'file-upload-file-validation-example'
---
::


## API

### Props

:component-props

### Slots

:component-slots

### Emits

:component-emits

### Expose

When accessing the component via a template ref, you can use the following:

| Name | Type |
| ---- | ---- |
| `fileInputRef`{lang="ts-type"} | `Ref<HTMLInputElement \| null>`{lang="ts-type"} |

## Theme

:component-theme
1 change: 1 addition & 0 deletions playground/app/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const components = [
'dropdown-menu',
'form',
'form-field',
'file-upload',
'input',
'input-menu',
'input-number',
Expand Down
20 changes: 20 additions & 0 deletions playground/app/pages/components/file-upload.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<script lang="ts" setup>
import type { FileUploadItem } from '@nuxt/ui'

type UploadWithStatus = FileUploadItem<{ status: 'pending' | 'uploading' | 'done', progress?: number }>
const files = ref<UploadWithStatus[]>()
const testFile = ref<UploadWithStatus[]>([{
file: new File([''], 'test.txt'),
status: 'pending'
}])
</script>

<template>
<div class="flex flex-col gap-4 items-center">
<UFileUpload v-model="files" size="xs" />
<UFileUpload v-model="testFile" size="sm" />
<UFileUpload size="md" />
<UFileUpload size="lg" disabled />
<UFileUpload size="xl" :multiple="true" />
</div>
</template>
Loading