Vue JS multiple file upload with custom file name

Issue

This Content is from Stack Overflow. Question asked by Pieter

In my Vue app I have a product edit page where I can upload multiple attachments. For each attachment, the user must be able to enter a custom file name.

Therefore I have an array containing all the attachment items, where ach attachment item is rendered by a component called ‘AttachmentsItem’.

The user can add extra attachments, or even remove them again.

The issue that I have is when removing items, for example:

Let’s say I added 4 attachments in total. When I remove attachment number 3, the item is being removed correctly from the array. However, the ‘file input html element’ still seems to hold the file from attachment number 3 (which just has been removed).

What is the cause of this and how can I fix it?

The code of my parent component:

<template>
    <div class="products-edit">

        <!-- Spinner -->
        <spinner v-show="isLoading"></spinner>

        <!-- Form nl -->
        <section v-if="!isLoading" class="form">

            <form @submit.prevent="updateProduct">

                <div class="form-group-wrapper">

                    <!-- Featured image -->
                    <div class="form-group">

                        <h2>Product afbeelding</h2>

                        <div class="img-placeholder">
                            <img v-if="!product_details" src="/assets/images/placeholder.png" alt="Featured image">
                            <img v-else-if="product_details.featured_image"
                                :src="'/images/' + product_details.featured_image" alt="Featured image">
                            <img v-else src="/assets/images/placeholder.png" alt="Featured image">
                        </div>

                        <input type="file" ref="featured_image" id="featured_image" name="featured_image"
                            v-on:change="handleFileUpload()">

                        <span v-if="errors && errors.featured_image" class="error-message">{{ errors.featured_image[0]
                            }}</span>

                    </div>

                    <!-- Attachments -->
                    <div class="form-group">
                        <h2>Bijlagen</h2>

                        <AttachmentsItem @removeAttachment="removeAttachment" @addAttachment="addAttachment" v-for="(attachment,index) in attachments" :key="index" :attachment="attachment" :index="index"></AttachmentsItem>
                    </div>

                </div>

                <!-- Submit button -->
                <div class="form-group-wrapper">

                    <div class="form-group">

                        <button class="btn btn-primary btn-submit">
                            Opslaan

                            <spinner v-show="isPending"></spinner>
                        </button>

                        <p v-show="success" class="successMessage">
                            Wijzigigen opgeslagen
                            <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
                                stroke="currentColor" stroke-width="2">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
                            </svg>
                        </p>

                    </div>

                </div>

            </form>

        </section>

    </div>
</template>

<script>
import { SET_PAGE_TITLE, SET_PAGE_CTA, IS_LOADING } from '../../constants';
import spinner from '../../components/spinner.vue';
import AttachmentsItemVue from '../../components/AttachmentsItem.vue';
import AttachmentsItem from '../../components/AttachmentsItem.vue';

export default {
    name: 'ProductsEdit',
    components: {
    spinner,
    AttachmentsItemVue,
    AttachmentsItem
},
    mounted() {
        this.setPageTitle;
        this.loadData();
    },
    data() {
        return {
            product: null,
            product_details: null,

            featured_image: '',
            attachments: [
                {
                    file: '',
                    name: '',
                }
            ],

            isLoading: true,
            isPending: false,
            errors: null,
            success: false,
        }
    },
    methods: {
        removeAttachment(index) {
            if (this.attachments.length > 1) {
                this.attachments.splice(index, 1);
            } else {
                this.attachments = [{}];
            }
        },
        addAttachment() {
            this.attachments.push({});
        },
        handleFileUpload() {
            this.featured_image = this.$refs.featured_image.files[0];
        },
        loadData() {
            axios.get(`/items/${this.$route.params.productId}/edit`)
                .then((res) => {
                    this.product = res.data.product;
                    this.product_details = res.data.product_details;

                    this.isLoading = false;
                })
                .catch((err) => {
                    console.log(err);

                    this.isLoading = false;
                });
        },
        updateProduct() {
            this.isPending = true;

            let f = new FormData();

            f.append('featured_image', this.featured_image);
            f.append('attachments', this.attachments);

            axios.post(`/items/${this.$route.params.productId}/update`, f)
                .then((res) => {
                    this.isPending = false;
                    this.success = true;

                    this.product_details = res.data.product_details;

                    setTimeout(() => {
                        this.success = false;
                    }, 3000);
                })
                .catch((err) => {
                    this.errors = err.response.data.errors;

                    this.isPending = false;
                });
        },
    },
    computed: {
        setPageTitle() {
            this.$store.dispatch('appStore/' + SET_PAGE_TITLE, 'Product aanpassen');
            this.$store.dispatch('appStore/' + SET_PAGE_CTA, { 'title': 'Terug', 'url': '/products' });
        },
        currentLang() {
            return this.$store.state.appStore.currentLanguage;
        },
    },
}
</script>

The code of my child component:

<template>
    <div class="attachments-item">
        <label :for="'attachment-' + index">Bijlage {{index + 1}}</label>
        
        <div class="input-group">
            <input type="file" :id="'attachment-' + index" ref="attachmentFile" v-on:change="handleFileUpload()" accept=".pdf,.docx">

            <input type="text" placeholder="Bijlage naam" v-model="attachment.name">

            <div class="action-buttons">
                <svg @click="addAttachment" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
                    stroke="currentColor" class="w-6 h-6">
                    <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
                </svg>

                <svg @click="removeAttachment" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
                    class="w-6 h-6">
                    <path stroke-linecap="round" stroke-linejoin="round" d="M15 12H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" />
                </svg>
            </div>
        </div>

    </div>
</template>

<script>
export default {
    name: 'AttachmentsItem',
    props: {
        attachment: {
            type: Object,
            required: true,
        },
        index: {
            type: Number,
            required: true,
        },
    },
    methods: {
        addAttachment() {
            this.$emit('addAttachment');
        },
        removeAttachment() {
            this.$emit('removeAttachment', this.index);
        },
        handleFileUpload() {
            this.attachment.file = this.$refs.attachmentFile.files[0];
        },
    },
}
</script>



Solution

This question is not yet answered, be the first one who answer using the comment. Later the confirmed answer will be published as the solution.

This Question and Answer are collected from stackoverflow and tested by JTuto community, is licensed under the terms of CC BY-SA 2.5. - CC BY-SA 3.0. - CC BY-SA 4.0.

people found this article helpful. What about you?