vueuse icon indicating copy to clipboard operation
vueuse copied to clipboard

feat(useFileDialog): add MaybRef to multiple, accept, capture, reset, and directory

Open hunterwilhelm opened this issue 10 months ago • 5 comments

Before submitting the PR, please make sure you do the following

  • [x] Read the Contributing Guidelines.
  • [x] Read the Pull Request Guidelines.
  • [x] Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
  • [x] Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123).
  • [x] Ideally, include relevant tests that fail without this PR but pass with it.
⚠️ Slowing down new functions

Warning: Slowing down new functions

As the VueUse audience continues to grow, we have been inundated with an overwhelming number of feature requests and pull requests. As a result, maintaining the project has become increasingly challenging and has stretched our capacity to its limits. As such, in the near future, we may need to slow down our acceptance of new features and prioritize the stability and quality of existing functions. Please note that new features for VueUse may not be accepted at this time. If you have any new ideas, we suggest that you first incorporate them into your own codebase, iterate on them to suit your needs, and assess their generalizability. If you strongly believe that your ideas are beneficial to the community, you may submit a pull request along with your use cases, and we would be happy to review and discuss them. Thank you for your understanding.


Description

Fixes #4812

add MaybRef to multiple, accept, capture, reset, and directory

Additional context

These 5 parameters are only used upon invocation of open, so they can be easily updated using ref. The alternative was to rerender the entire component to update the composable.

hunterwilhelm avatar Jun 11 '25 16:06 hunterwilhelm

AMAZING LGTM!

MatthewAry avatar Jun 11 '25 16:06 MatthewAry

If we add this as MaybeRef we should also consider what we want to do it the ref updates

OrbisK avatar Jun 12 '25 12:06 OrbisK

@OrbisK Already taken care of, it adds toValue everywhere they are being used, which happens to be all in the open function.

hunterwilhelm avatar Jun 12 '25 14:06 hunterwilhelm

@hunterwilhelm this is true, but we are not reacting if the ref value changes

OrbisK avatar Jun 13 '25 11:06 OrbisK

@OrbisK I see! I changed it so the refs update the element properties when the refs change, including the input element param. Is there anything else I should change?

hunterwilhelm avatar Jun 13 '25 21:06 hunterwilhelm

@OrbisK Any chance we can see this merged?

MatthewAry avatar Jul 07 '25 20:07 MatthewAry

Can someone confirm that this is working on mac/safari?

OrbisK avatar Jul 08 '25 06:07 OrbisK

@OrbisK Just tested it on Safari on Mac and Safari on iPhone, all the features work perfectly. I wrote a UI for each test.

Full test with a UI
<script setup lang="ts">
import { useFileDialog } from '@vueuse/core'
import { shallowRef } from 'vue'

// 1. Basic usage (default)
const {
  files: filesDefault,
  open: openDefault,
  reset: resetDefault,
  onCancel: onCancelDefault,
  onChange: onChangeDefault,
} = useFileDialog()
onChangeDefault((files) => {
  // do something with files
})
onCancelDefault(() => {
  // do something on cancel
})

// 2. Initial files as Array<File>
const initialFilesArr = shallowRef<File[]>([new File(['baz'], 'baz.txt', { type: 'text/plain' })])
const {
  files: filesArr,
  open: openArr,
  reset: resetArr,
} = useFileDialog({ initialFiles: initialFilesArr.value })

// 3. Initial files as FileList (simulate with DataTransfer)
const initialFilesList = shallowRef<FileList | undefined>(undefined)
if (typeof window !== 'undefined') {
  const dt = new DataTransfer()
  dt.items.add(new File(['foo'], 'foo.txt', { type: 'text/plain' }))
  dt.items.add(new File(['bar'], 'bar.txt', { type: 'text/plain' }))
  initialFilesList.value = dt.files
}
const {
  files: filesList,
  open: openList,
  reset: resetList,
} = useFileDialog({ initialFiles: initialFilesList.value })

// 4. Custom input element
const customInput = shallowRef<HTMLInputElement | undefined>(undefined)
const {
  open: openCustomInput,
  files: filesCustomInput,
} = useFileDialog({ input: customInput.value })

// 5. Input as template ref (shallowRef)
const inputRef = shallowRef<HTMLInputElement>()
const {
  open: openInputRef,
  files: filesInputRef,
} = useFileDialog({ input: inputRef.value })

// 6. Multiple as ref
const multipleRef = shallowRef(true)
const {
  open: openMultiple,
  files: filesMultiple,
} = useFileDialog({ multiple: multipleRef })

// 7. Accept as ref
const acceptRef = shallowRef('image/*')
const {
  open: openAccept,
  files: filesAccept,
} = useFileDialog({ accept: acceptRef })

// 8. Directory as ref
const directoryRef = shallowRef(false)
const {
  open: openDirectory,
  files: filesDirectory,
} = useFileDialog({ directory: directoryRef })

// 9. Reset as ref
const resetOptRef = shallowRef(false)
const {
  open: openResetOpt,
  files: filesResetOpt,
} = useFileDialog({ reset: resetOptRef })

// 10. Capture as ref
const captureRef = shallowRef('user')
const {
  open: openCapture,
  files: filesCapture,
} = useFileDialog({ capture: captureRef })

// Helper for showing file names
function fileNames(files: FileList | File[] | null) {
  if (!files)
    return 'None'
  const arr = Array.from(files)
  return arr.map(f => f.name).join(', ')
}
</script>

<template>
  <h2>1. Default</h2>
  <button type="button" @click="openDefault()">
    Choose files
  </button>
  <button type="button" :disabled="!filesDefault" @click="resetDefault()">
    Reset
  </button>
  <div v-if="filesDefault">
    <p>You have selected: <b>{{ `${filesDefault.length} ${filesDefault.length === 1 ? 'file' : 'files'}` }}</b></p>
    <ul>
      <li v-for="file of filesDefault" :key="file.name">
        {{ file.name }}
      </li>
    </ul>
  </div>

  <h2>2. Initial files as Array&lt;File&gt;</h2>
  <button type="button" @click="openArr()">
    Choose files
  </button>
  <button type="button" :disabled="!filesArr" @click="resetArr()">
    Reset
  </button>
  <div v-if="filesArr">
    <p>Selected: {{ fileNames(filesArr) }}</p>
  </div>

  <h2>3. Initial files as FileList</h2>
  <button type="button" @click="openList()">
    Choose files
  </button>
  <button type="button" :disabled="!filesList" @click="resetList()">
    Reset
  </button>
  <div v-if="filesList">
    <p>Selected: {{ fileNames(filesList) }}</p>
  </div>

  <h2>4. Custom input element</h2>
  <input ref="customInput" type="file" style="display:none">
  <button type="button" @click="openCustomInput()">
    Choose files (custom input)
  </button>
  <div v-if="filesCustomInput">
    <p>Selected: {{ fileNames(filesCustomInput) }}</p>
  </div>

  <h2>5. Input as template ref (shallowRef)</h2>
  <input ref="inputRef" type="file" style="display:none">
  <button type="button" @click="openInputRef()">
    Choose files (inputRef)
  </button>
  <div v-if="filesInputRef">
    <p>Selected: {{ fileNames(filesInputRef) }}</p>
  </div>

  <h2>6. Multiple as ref</h2>
  <button type="button" @click="openMultiple()">
    Choose files (multiple: {{ multipleRef }})
  </button>
  <button @click="multipleRef = !multipleRef">
    Toggle multiple
  </button>
  <div v-if="filesMultiple">
    <p>Selected: {{ fileNames(filesMultiple) }}</p>
  </div>

  <h2>7. Accept as ref</h2>
  <button type="button" @click="openAccept()">
    Choose files (accept: {{ acceptRef }})
  </button>
  <button @click="acceptRef = acceptRef === 'image/*' ? 'video/*' : 'image/*'">
    Toggle accept
  </button>
  <div v-if="filesAccept">
    <p>Selected: {{ fileNames(filesAccept) }}</p>
  </div>

  <h2>8. Directory as ref</h2>
  <button type="button" @click="openDirectory()">
    Choose files (directory: {{ directoryRef }})
  </button>
  <button @click="directoryRef = !directoryRef">
    Toggle directory
  </button>
  <div v-if="filesDirectory">
    <p>Selected: {{ fileNames(filesDirectory) }}</p>
  </div>

  <h2>9. Reset as ref</h2>
  <button type="button" @click="openResetOpt()">
    Choose files (reset: {{ resetOptRef }})
  </button>
  <button @click="resetOptRef = !resetOptRef">
    Toggle reset
  </button>
  <div v-if="filesResetOpt">
    <p>Selected: {{ fileNames(filesResetOpt) }}</p>
  </div>

  <h2>10. Capture as ref</h2>
  <button type="button" @click="openCapture()">
    Choose files (capture: {{ captureRef }})
  </button>
  <button @click="captureRef = captureRef === 'user' ? 'environment' : 'user'">
    Toggle capture
  </button>
  <div v-if="filesCapture">
    <p>Selected: {{ fileNames(filesCapture) }}</p>
  </div>
</template>

hunterwilhelm avatar Jul 08 '25 15:07 hunterwilhelm

@OrbisK Thanks for approving 🚀

hunterwilhelm avatar Jul 09 '25 14:07 hunterwilhelm

@ilyaliao @OrbisK Added the suggested changes, it should be good to go 🚀

hunterwilhelm avatar Jul 18 '25 18:07 hunterwilhelm

@ilyaliao @OrbisK Added the suggested change, anything else I should add?

hunterwilhelm avatar Jul 21 '25 14:07 hunterwilhelm

Open in StackBlitz

@vueuse/components

npm i https://pkg.pr.new/@vueuse/components@4813
@vueuse/core

npm i https://pkg.pr.new/@vueuse/core@4813
@vueuse/electron

npm i https://pkg.pr.new/@vueuse/electron@4813
@vueuse/firebase

npm i https://pkg.pr.new/@vueuse/firebase@4813
@vueuse/integrations

npm i https://pkg.pr.new/@vueuse/integrations@4813
@vueuse/math

npm i https://pkg.pr.new/@vueuse/math@4813
@vueuse/metadata

npm i https://pkg.pr.new/@vueuse/metadata@4813
@vueuse/nuxt

npm i https://pkg.pr.new/@vueuse/nuxt@4813
@vueuse/router

npm i https://pkg.pr.new/@vueuse/router@4813
@vueuse/rxjs

npm i https://pkg.pr.new/@vueuse/rxjs@4813
@vueuse/shared

npm i https://pkg.pr.new/@vueuse/shared@4813

commit: 8429ff1

pkg-pr-new[bot] avatar Jul 28 '25 04:07 pkg-pr-new[bot]

Why not MaybeRefOrGetter?

VividLemon avatar Jul 28 '25 17:07 VividLemon