vuejs3shadcnui

InvalidStateError when trying to upload multiple photos using shadcn-vue forms


Learning vue3, trying to create a form that takes multiple images as input for submission, but I keep getting these errors/warnings:

chunk-HSMNR5CP.js?v=f7f38f4d:2113 [Vue warn]: Invalid prop: type check failed for prop "modelValue". Expected String | Number, got Array  
  at <Input type="file" accept="image/*" multiple="multiple"  ... > 
  at <PrimitiveSlot id="radix-v-7-form-item" aria-describedby="radix-v-7-form-item-description" aria-invalid=false > 
  at <FormControl > 
  at <FormItem > 
  at <Field name="photos" > 
  at <NewLogForm > 
  at <HomeView onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< Proxy(Object) {__v_skip: true} > > 
  at <RouterView > 
  at <App>

Global error: InvalidStateError: Failed to set the 'value' property on 'HTMLInputElement': This input element accepts a filename, which may only be programmatically set to the empty string.

My code:

<script setup lang="ts">
import { Button } from '@/components/ui/button';
import {
    FormControl,
    FormDescription,
    FormField,
    FormItem,
    FormLabel,
    FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';

import { toTypedSchema } from '@vee-validate/zod';
import { useForm } from 'vee-validate';
import { ref } from 'vue';
import * as z from 'zod';

const image_list = ref([]);
const preview_list = ref([]);

const formSchema = toTypedSchema(z.object({
    title: z
        .string({
            required_error: 'Title is required.',
        })
        .min(2, {
            message: 'Title must be at least 2 characters.',
        }),

    photos: z.array(z.instanceof(File).refine((file) => file.size < 3000000, {
        message: 'Your file size must be less than 3 MB.',
    })).optional(),
}));

function deleteImageFromMultiple(index: any) {
    if (index > -1) {
        image_list.value.splice(index, 1);
        preview_list.value.splice(index, 1);
    }
}

function previewMultiImage(event: any) {
    reset();
    console.log(event);
    const input = event.target;
    let count = input.files.length;
    let index = 0;
    if (input.files) {
        while (count--) {
            const reader = new FileReader();
            reader.onload = (e) => {
                preview_list.value.push(e.target!.result);
            };
            image_list.value.push(input.files[index]);
            reader.readAsDataURL(input.files[index]);
            index++;
        }
    }
}

function reset() {
    image_list.value = [];
    preview_list.value = [];
}

const { handleSubmit } = useForm({
    validationSchema: formSchema,
});

const onSubmit = handleSubmit((values) => {
    console.log('Form submitted!', values);
});
</script>

<template>
    <form class="w-2/3 space-y-6" @submit="onSubmit">
        <!-- <form class="w-2/3 space-y-6" @submit.prevent="onSubmit"> -->
        <FormField v-slot="{ componentField }" name="title">
            <FormItem>
                <FormLabel>Title</FormLabel>
                <FormControl>
                    <Input type="text" placeholder="asd" v-bind="componentField" />
                </FormControl>
                <FormDescription>
                    This is the title you will give this entry.
                </FormDescription>
                <FormMessage />
            </FormItem>
        </FormField>
        <FormField v-slot="{ componentField }" name="photos">
            <FormItem>
                <FormLabel>Photos</FormLabel>
                <FormControl>
                    <Input type="file" accept="image/*" multiple="multiple" @change="previewMultiImage"
                        v-bind="componentField" />
                </FormControl>
                <p>Preview Here:</p>
                <template v-if="preview_list.length">
                    <div v-for="item, index in preview_list" :key="index">
                        <img :src="item" class="img-fluid" />
                        <p class="mb-0">file name: {{ image_list[index]!.name }}</p>
                        <p>size: {{ image_list[index].size / 1024 }}KB</p>
                        <button type="button" @click="deleteImageFromMultiple(index)">Delete This
                            Image</button>
                    </div>
                </template>
            </FormItem>
        </FormField>
        <Button type="submit">
            Submit
        </Button>
    </form>
</template>

How do I fix these errors? I'm not too sure how the {componentField} vslot/vbind works. I tried changing a few things related to the FieldSlotProps in my template but that didn't seem to work either. If I don't add the componentField in vslot, or put v-bind="{componentField}", photos will be undefined.

FieldSlotProps interface in vee-validate.d.ts:

interface FieldSlotProps<TValue = unknown> extends Pick<FieldContext, 'validate' | 'resetField' | 'handleChange' | 'handleReset' | 'handleBlur' | 'setTouched' | 'setErrors' | 'setValue'> {
    field: FieldBindingObject<TValue>;
    componentField: ComponentFieldBindingObject<TValue>;
    value: TValue;
    meta: FieldMeta<TValue>;
    errors: string[];
    errorMessage: string | undefined;
    handleInput: FieldContext['handleChange'];
}

Solution

  • Solution:

    <FormField v-slot="{ handleChange }" name="photos">
      <Input
        type="file"
        accept="image/*"
        multiple="multiple"
        @change="(event) => {
          handleChange(event);
          previewMultiImage(event);
        }"
      />
    </FormField>
    

    file input does not support v-model, because you cannot force the user to select a file on disk. When you use v-bind componentField, it tries to build a two-way binding because of prop modelValue in componentField.

    And vee-validate document has some explanations https://vee-validate.logaretm.com/v4/api/field/

    enter image description here

    Another suggestion: you can watch values from useForm to achieve previewing.

    const { values } = useForm()
    watch(values.photos, (photos) => {
      console.log('handle photos changes');
    })