Last active
February 22, 2025 19:11
-
-
Save blairdow/9dc3766803ed2c69834b4e1fe3f8220d to your computer and use it in GitHub Desktop.
Vue 3 File Upload Input Component (with Button and ProgressBar child components)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <template> | |
| <div> | |
| <label :for="name"> | |
| <strong>{{ label }}</strong> | |
| </label> | |
| <div class="input-container mt-3 mb-4"> | |
| <span class="mb-5 z-1">Drop Files Here or</span> | |
| <input | |
| type="file" | |
| :accept="accept" | |
| :name="name" | |
| :id="name" | |
| :multiple="multiple" | |
| class="custom-file-input" | |
| @change="handleFileChange($event)" | |
| /> | |
| </div> | |
| <div class="file-errors-container" v-if="fileErrors.length > 0 && filesArray.length > 0"> | |
| <p class="error text-danger" v-for="error in fileErrors" :key="error"> | |
| {{ error }} | |
| </p> | |
| </div> | |
| <div class="progress-bar-container mb-2" v-for="file in filesArray" :key="file.name"> | |
| <div class="d-flex align-items-center"> | |
| <ProgressBar | |
| class="w-100 me-2" | |
| :progressBarWidth="file.progressBarWidth" | |
| :fillColor="'#bacdff'" | |
| ></ProgressBar> | |
| <div class="lh-1">{{ file.progressBarWidth }}%</div> | |
| </div> | |
| <div class="file-name"> | |
| {{ file.name }} ({{ fileSizeInKB(file.size) }} kb) | |
| <CustomButton @click="removeFile(file)" :transparent="true" style="color: red"> | |
| <i class="fa-regular fa-circle-xmark"></i> | |
| </CustomButton> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script> | |
| import CustomButton from '@/components/widgets/Button.vue' | |
| import ProgressBar from '@/components/widgets/ProgressBar.vue' | |
| export default { | |
| name: 'FileUploader', | |
| components: { CustomButton, ProgressBar }, | |
| props: { | |
| accept: { type: String, default: 'application/pdf,pdf,image/png,png,image/jpeg,jpg,jpeg' }, | |
| name: { type: String, required: true }, | |
| label: { type: String, default: 'Upload File Here' }, | |
| multiple: { type: Boolean, default: false }, | |
| maxFiles: { type: Number, default: 3 }, | |
| maxSize: { type: Number, default: 5 }, | |
| }, | |
| emits: ['update:modelValue'], | |
| data() { | |
| return { | |
| fileErrors: [], | |
| filesArray: [], | |
| progressBarWidth: 0, | |
| } | |
| }, | |
| computed: { | |
| maxFilesComputed() { | |
| return this.multiple ? this.maxFiles : 1 | |
| }, | |
| }, | |
| methods: { | |
| fileSizeInMB(fileSize) { | |
| return Math.round((fileSize / 1024 / 1024) * 100) / 100 | |
| }, | |
| fileSizeInKB(fileSize) { | |
| if (fileSize > 0) { | |
| return Math.round(fileSize / 1024) | |
| } else return fileSize | |
| }, | |
| handleFileChange(e) { | |
| this.fileErrors = [] | |
| this.progressBarWidth = 0 | |
| if (!this.multiple) { | |
| this.filesArray = [] | |
| } | |
| if (e.target.files && e.target.files[0]) { | |
| const files = e.target.files | |
| this.isFilesNumberValid(files) //check against maxFilesComputed number | |
| for (let index = 0; index < this.maxFilesComputed - this.filesArray.length; index++) { | |
| //only process files up to maxFiles length, including already processed files | |
| let file = files[index] | |
| if (file && this.isFileValid(file)) { | |
| // const file = e.target.files[0] | |
| let reader = new FileReader() | |
| reader.onloadstart = () => { | |
| file.progressBarWidth = 0 | |
| file.loading = true | |
| } | |
| reader.onprogress = (event) => { | |
| if (event.lengthComputable) { | |
| file.progressBarWidth = Math.floor((event.loaded / event.total) * 100) | |
| } | |
| } | |
| reader.onload = () => { | |
| this.filesArray.push(file) | |
| file.progressBarWidth = 100 | |
| file.loading = false | |
| this.$nextTick(() => { | |
| this.$emit('update:modelValue', this.filesArray) //tell parent to update v-model | |
| }) | |
| } | |
| reader.readAsDataURL(file) | |
| } | |
| } | |
| } | |
| }, | |
| removeFile(file) { | |
| const index = this.filesArray.indexOf(file) | |
| console.log(index) | |
| if (index > -1) { | |
| this.filesArray.splice(index, 1) | |
| } | |
| }, | |
| isFileSizeValid(fileSize, fileName) { | |
| let sizeInMB = this.fileSizeInMB(fileSize) | |
| if (sizeInMB > this.maxSize) { | |
| if ( | |
| this.fileErrors.indexOf(`${fileName} is larger than 5MB, please upload a smaller file.`) < | |
| 0 | |
| ) { | |
| this.fileErrors.push(`${fileName} is larger than 5MB, please upload a smaller file.`) | |
| } | |
| } | |
| return sizeInMB <= this.maxSize | |
| }, | |
| isFileTypeValid(fileExtension, fileName) { | |
| if (this.accept.split(',').includes(fileExtension) === false) { | |
| if ( | |
| this.fileErrors.indexOf( | |
| `${fileName} is not an accepted file type, please upload a different file.`, | |
| ) < 0 | |
| ) { | |
| this.fileErrors.push( | |
| `${fileName} is not an accepted file type, please upload a different file.`, | |
| ) | |
| } | |
| } | |
| return this.accept.split(',').includes(fileExtension) | |
| }, | |
| isFileValid(file) { | |
| return ( | |
| this.isFileSizeValid(file.size, file.name) && | |
| this.isFileTypeValid(file.name.split('.').pop(), file.name) | |
| ) | |
| }, | |
| isFilesNumberValid(files) { | |
| if (files.length > this.maxFilesComputed - this.filesArray.length) { | |
| if ( | |
| this.fileErrors.indexOf( | |
| `The maximum number of uploaded files is ${this.maxFilesComputed}, only the first ${this.maxFilesComputed} selected have been processed.`, | |
| ) < 0 | |
| ) { | |
| this.fileErrors.push( | |
| `The maximum number of uploaded files is ${this.maxFilesComputed}, only the first ${this.maxFilesComputed} selected have been processed.`, | |
| ) | |
| } | |
| } | |
| }, | |
| }, | |
| } | |
| </script> | |
| <style lang="scss" scoped> | |
| .input-container { | |
| min-height: 200px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border: 2px dashed var(--color-black); | |
| background-color: var(--color-white); | |
| position: relative; | |
| } | |
| .custom-file-input { | |
| color: transparent; | |
| position: absolute; | |
| top: 0; | |
| bottom: 0; | |
| right: 0; | |
| left: 0; | |
| cursor: pointer; | |
| &:hover { | |
| background: var(--color-gray); | |
| } | |
| } | |
| .custom-file-input::-webkit-file-upload-button { | |
| visibility: hidden; | |
| } | |
| .custom-file-input::before { | |
| content: 'Select File'; | |
| color: black; | |
| display: inline-block; | |
| background: -webkit-linear-gradient(top, #f9f9f9, #e3e3e3); | |
| border: 1px solid #999; | |
| border-radius: 3px; | |
| padding: 5px 8px; | |
| outline: none; | |
| white-space: nowrap; | |
| user-select: none; | |
| -webkit-user-select: none; | |
| text-shadow: 1px 1px #fff; | |
| font-weight: 700; | |
| font-size: 10pt; | |
| position: absolute; | |
| top: 55%; | |
| left: 50%; | |
| translate: -50% -50%; | |
| } | |
| .custom-file-input:hover::before { | |
| border-color: black; | |
| } | |
| .custom-file-input:active { | |
| outline: 0; | |
| } | |
| .custom-file-input:active::before { | |
| background: -webkit-linear-gradient(top, #e3e3e3, #f9f9f9); | |
| } | |
| </style> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <template> | |
| <div class="progressbar"> | |
| <svg width="100%" :height="height"> | |
| <rect | |
| x="0" | |
| y="0" | |
| :width="progressBarWidth + '%'" | |
| :height="height" | |
| :style="`fill: ${fillColor}`" | |
| /> | |
| <rect | |
| x="0" | |
| y="0" | |
| width="100%" | |
| :height="height" | |
| :style="`stroke: ${strokeColor}; fill: none`" | |
| /> | |
| </svg> | |
| </div> | |
| </template> | |
| <script setup> | |
| defineOptions({ | |
| name: 'ProgressBar', | |
| }) | |
| defineProps({ | |
| progressBarWidth: { | |
| type: Number, | |
| }, | |
| height: { type: String, default: '15px' }, | |
| fillColor: { | |
| type: String, | |
| default: '#F2FD7C', | |
| }, | |
| strokeColor: { | |
| type: String, | |
| default: '#808080', | |
| }, | |
| }) | |
| </script> | |
| <style scoped></style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment