How do I make this multi-steps form component dynamic using vue3?

Viewed 23

Using this tutorial https://vueschool.io/articles/vuejs-tutorials/building-a-multi-step-form-with-petite-vue/, I was able to create 3 components for a multi-steps form setup which gave the same result as the tutorial.

I have a MultiStepsForm with 2 child components-FormFields and StepsIndicator. These are their codes:

MultiStepsForm:

<template>
    <div class="py-8">
        <div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white dark:bg-gray-800 border border-gray-200 overflow-hidden sm:rounded-lg rounded">
                <div class="p-6 overflow-x-auto">
                    <form @submit.prevent="">
                        <div>
                            <steps-indicator :steps="steps" :currentStep="currentStep" :stepsCount="totalSteps"></steps-indicator>
                        </div>
                            <div v-for="(fieldKeys, step) in steps">
                                <div v-if="currentStep === step">
                                    <div v-for="(field, index) in fieldKeys">
                                    <form-fields :field="fields[field]">

                                    </form-fields>
                                    </div>
                                </div>
                            </div>
                        <div class="flex items-center justify-end mt-4">
                            <jet-button v-if="!isFirstStep" @click.prevent="previousStep" class="ml-4">
                                Previous
                            </jet-button>
                            <jet-button v-if="!isLastStep" @click.prevent="nextStep" class="ml-4">
                                Next
                            </jet-button>
                            <jet-button v-if="isLastStep" @click.prevent="submit" class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
                                Submit
                            </jet-button>                            
                        </div>
                    </form>  
                </div>
            </div>
        </div>
    </div>
</template>

<script setup>
import JetButton from '@/Jetstream/Button.vue'
import JetLabel from '@/Jetstream/Label.vue'
import { useForm } from '@inertiajs/inertia-vue3'
import StepsIndicator from '@/Jetstream/StepsIndicator.vue'
import FormFields from '@/Jetstream/FormFields.vue'
import { ref, computed } from 'vue'

const props = defineProps({
    fields: {
        field: {
            label: '',
            value: ''
        }
    },
    url: String,
    steps: Array
})

const currentStep = ref(0)

const steps = props.steps

const form = useForm({
    fields: props.fields
})

const isFirstStep = computed(() => {
    return currentStep.value === 0
})

const totalSteps = computed(() => {
    return steps.length
})

const isLastStep = computed(() => {
    return currentStep.value === totalSteps.value - 1
})

const currentFields = computed(() => {
    return steps[currentStep.value]
})

function previousStep() {
    if (isFirstStep.value) return
    currentStep.value--
}

function nextStep() {
    if (isLastStep.value) return
    currentStep.value++
}

function submit() {
    form.post(route(props.url), {
        onFinish: () => form.reset()
    })
}
</script>

FormFields:

<template>
    <div class="relative">
        <div class="col-span-6 sm:col-span-4">
            <label class="label">
            {{field.label}}
            <input class="mt-1 block w-full" type="text" v-model="field.value"/>
            </label>
        </div>
    </div>
</template>

<script setup>
import JetValidationErrors from '@/Jetstream/ValidationErrors.vue'
import JetInputError from '@/Jetstream/InputError.vue'

defineProps({
    field: Object
})
</script>

StepsIndicator:

<template>
    <div class="flex items-stretch gap-2">
    <div
        v-for="step in stepsCountWithSuccessPage"
        class="h-2 w-full rounded text-purple-500"
        style="border: 1px solid;"
        :class="{'bg-purple-500 ': step - 1 <= currentStep}"
    ></div>
    </div>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
    currentStep: Number,
    steps: Array,
    stepsCount: Number
})

const stepsCountWithSuccessPage = computed(() => {
    return props.stepsCount + 1
})
</script>

I was able to produce these-see screenshots-using the hardcoded data from the tutorial in the component where I want the multi-steps form. first page second page last page

This is the component where I tested/used the MultiStepsForm:

<template>
//
    <div class="pt-12 mt-12">
        <div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
            <multi-steps-form :fields="fields" :url="url" :steps="steps"></multi-steps-form>
        </div>
    </div>
</div>
</template>

<script setup>
//
import MultiStepsForm from '@/Jetstream/MultiStepsForm.vue'

//

const fields = {
    name: {
      label: "Name",
      value: "",
    },
    email: {
      label: "Email",
      value: "",
    },
    address: {
      label: "Address",
      value: "",
    },
    city: {
      label: "City",
      value: "",
    },
    state: {
      label: "State",
      value: "",
    },
    zip: {
      label: "Zip",
      value: "",
    },
    donationAmount: {
      label: "Donation Amount",
      value: "",
    }
  }

  const url = 'url to process form data'

  const steps = [
    ["name", "email"],
    ["address", "city", "state", "zip"],
    ["donationAmount"]
]
</script>

I'm now stuck at how to make the fields and steps props dynamic enough to be used for data of different types (text and files) and length. As well as how to keep using my existing form validation.

I can't figure out how to use it for this kind of form:

<template>
    <jet-form-section @submitted="verifyIdentity">
        <template #title>
            Identity Verification
        </template>

        <template #description>
            Verify your identity by filling out this form.
        </template>

        <template #form>
            <!-- First Name -->
            <div class="col-span-6 sm:col-span-4">
                <jet-label for="first_name" value="First Name" />
                <jet-input id="first_name" type="text" class="mt-1 block w-full" v-model="form.first_name" autocomplete="first_name" />
                <jet-input-error :message="form.errors.first_name" class="mt-2" />
            </div>
            <!-- Middle Name -->
            <div class="col-span-6 sm:col-span-4">
                <jet-label for="middle_name" value="Middle Name" />
                <jet-input id="middle_name" type="text" class="mt-1 block w-full" v-model="form.middle_name" autocomplete="middle_name" />
                <jet-input-error :message="form.errors.middle_name" class="mt-2" />
            </div>
            <!-- Last Name -->
            <div class="col-span-6 sm:col-span-4">
                <jet-label for="last_name" value="Last Name" />
                <jet-input id="last_name" type="text" class="mt-1 block w-full" v-model="form.last_name" autocomplete="last_name" />
                <jet-input-error :message="form.errors.last_name" class="mt-2" />
            </div>
            <!-- Email -->
            <div class="col-span-6 sm:col-span-4">
                <jet-label for="email" value="Email" />
                <jet-input id="email" type="email" class="mt-1 block w-full" v-model="form.email" />
                <jet-input-error :message="form.errors.email" class="mt-2" />
            </div>
            <!-- Mobile Phone Numbe -->
            <div class="col-span-6 sm:col-span-4">
                <jet-label for="mobile_phone_number" value="Mobile Phone Number" />
                <jet-input id="mobile_phone_number" type="text" class="mt-1 block w-full" v-model="form.mobile_phone_number" autocomplete="mobile_phone_number" />
                <jet-input-error :message="form.errors.mobile_phone_number" class="mt-2" />
            </div>
             <!-- Address -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="address" value="Contact Address" />
                <jet-input id="address" type="text" class="mt-1 block w-full" v-model="form.address" autocomplete="address" />
                <jet-input-error :message="form.errors.address" class="mt-2" />
            </div>
            <!-- Gender -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="gender" value="Gender" />
                <jet-input id="gender" type="text" class="mt-1 block w-full" v-model="form.gender" autocomplete="gender" />
                <jet-input-error :message="form.errors.gender" class="mt-2" />
            </div>
            <!-- Religion -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="religion" value="Religion" />
                <jet-input id="religion" type="text" class="mt-1 block w-full" v-model="form.religion" autocomplete="religion" />
                <jet-input-error :message="form.errors.religion" class="mt-2" />
            </div>
            <!-- Date of Birth -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="date_of_birth" value="Date of Birth" />
                <jet-input id="date_of_birth" type="date" class="mt-1 block w-full" v-model="form.date_of_birth" autocomplete="date_of_birth" />
                <jet-input-error :message="form.errors.date_of_birth" class="mt-2" />
            </div>
            <!-- Marital Status -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="marital_status" value="Marital Status" />
                <jet-input id="marital_status" type="text" class="mt-1 block w-full" v-model="form.marital_status" autocomplete="marital_status" />
                <jet-input-error :message="form.errors.marital_status" class="mt-2" />
            </div>
            <!-- Nationality -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="nationality" value="Nationality" />
                <jet-input id="nationality" type="text" class="mt-1 block w-full" v-model="form.nationality" autocomplete="nationality" />
                <jet-input-error :message="form.errors.nationality" class="mt-2" />
            </div>
            <!-- State of Origin -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="state_of_origin" value="State of Origin" />
                <jet-input id="state_of_origin" type="text" class="mt-1 block w-full" v-model="form.state_of_origin" autocomplete="state_of_origin" />
                <jet-input-error :message="form.errors.state_of_origin" class="mt-2" />
            </div>
            <!-- Local Govt Area -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="local_govt_area" value="Local Government Area" />
                <jet-input id="local_govt_area" type="text" class="mt-1 block w-full" v-model="form.local_govt_area" autocomplete="local_govt_area" />
                <jet-input-error :message="form.errors.local_govt_area" class="mt-2" />
            </div>
            <!-- Mother's Maiden Name -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="mother_maiden_name" value="Mother's Maiden Name" />
                <jet-input id="mother_maiden_name" type="text" class="mt-1 block w-full" v-model="form.mother_maiden_name" autocomplete="mother_maiden_name" />
                <jet-input-error :message="form.errors.mother_maiden_name" class="mt-2" />
            </div>
            <!-- Next of Kin's Name -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="next_of_kin_name" value="Your Next of Kin's Name" />
                <jet-input id="next_of_kin_name" type="text" class="mt-1 block w-full" v-model="form.next_of_kin_name" autocomplete="next_of_kin_name" />
                <jet-input-error :message="form.errors.next_of_kin_name" class="mt-2" />
            </div>
            <!-- Next of Kin Relationship -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="next_of_kin_relationship" value="Relationship With Your Next of Kin" />
                <jet-input id="next_of_kin_relationship" type="text" class="mt-1 block w-full" v-model="form.next_of_kin_relationship" autocomplete="next_of_kin_relationship" />
                <jet-input-error :message="form.errors.next_of_kin_relationship" class="mt-2" />
            </div>
            <!-- Next of Kin Mobile Phone Number -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="next_of_kin_mobile_phone_number" value="Your Next of Kin's Mobile Phone Number" />
                <jet-input id="next_of_kin_mobile_phone_number" type="text" class="mt-1 block w-full" v-model="form.next_of_kin_mobile_phone_number" autocomplete="next_of_kin_mobile_phone_number" />
                <jet-input-error :message="form.errors.next_of_kin_mobile_phone_number" class="mt-2" />
            </div>
            <!-- Next of Kin Address -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="next_of_kin_address" value="Your Next of Kin's Address" />
                <jet-input id="next_of_kin_address" type="text" class="mt-1 block w-full" v-model="form.next_of_kin_address" autocomplete="next_of_kin_address" />
                <jet-input-error :message="form.errors.next_of_kin_address" class="mt-2" />
            </div>
            <!-- Occupation -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="occupation" value="What Is Your Occupation" />
                <jet-input id="occupation" type="text" class="mt-1 block w-full" v-model="form.occupation" autocomplete="occupation" />
                <jet-input-error :message="form.errors.occupation" class="mt-2" />
            </div>
            <!-- Name of Employer -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="name_of_employer" value="Name of Employer" />
                <jet-input id="name_of_employer" type="text" class="mt-1 block w-full" v-model="form.name_of_employer" autocomplete="name_of_employer" />
                <jet-input-error :message="form.errors.name_of_employer" class="mt-2" />
            </div>
            <!-- Employer Address -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="employer_address" value="Employer Address" />
                <jet-input id="employer_address" type="text" class="mt-1 block w-full" v-model="form.employer_address" autocomplete="employer_address" />
                <jet-input-error :message="form.errors.employer_address" class="mt-2" />
            </div>
            <!-- Id Number -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="id_number" value="Identification Number" />
                <jet-input id="id_number" type="text" class="mt-1 block w-full" v-model="form.id_number" autocomplete="id_number" />
                <jet-input-error :message="form.errors.id_number" class="mt-2" />
            </div>
            <!-- ID Expiry Date -->
             <div class="col-span-6 sm:col-span-4">
                <jet-label for="id_expiry_date" value="ID Expiry Date" />
                <jet-input id="id_expiry_date" type="date" class="mt-1 block w-full" v-model="form.id_expiry_date" autocomplete="id_expiry_date" />
                <jet-input-error :message="form.errors.id_expiry_date" class="mt-2" />
            </div>
            <!-- Profile Photo -->
            <div class="col-span-6 sm:col-span-4" v-if="$page.props.jetstream.managesProfilePhotos">
                <!-- Profile Photo File Input -->
                <input type="file" class="hidden"
                            ref="photo"
                            @change="updatePhotoPreview">

                <jet-label for="photo" value="Photo" />

                <!-- Current Profile Photo -->
                <div class="mt-2" v-show="! photoPreview">
                    <img :src="user.profile_photo_url" :alt="user.first_name" class="rounded-full h-20 w-20 object-cover">
                </div>

                <!-- New Profile Photo Preview -->
                <div class="mt-2" v-show="photoPreview">
                    <span class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
                          :style="'background-image: url(\'' + photoPreview + '\');'">
                    </span>
                </div>

                <jet-secondary-button class="mt-2 mr-2" type="button" @click.prevent="selectNewPhoto">
                    Select A New Photo
                </jet-secondary-button>

                <jet-secondary-button type="button" class="mt-2" @click.prevent="deletePhoto" v-if="user.profile_photo_path">
                    Remove Photo
                </jet-secondary-button>

                <jet-input-error :message="form.errors.photo" class="mt-2" />
            </div>
            <!-- ID -->
            <div class="col-span-6 sm:col-span-4">
                <!-- ID File Input -->
                <input type="file" class="hidden"
                            ref="id"
                            @change="updateIdPreview">

                <jet-label for="id" value="ID" />

                <!-- Current ID -->
                <div class="mt-2" v-show="! idPreview">
                    <img :src="user.id_url" :alt="user.first_name" class="rounded-full h-20 w-20 object-cover">
                </div>

                <!-- New ID Preview -->
                <div class="mt-2" v-show="idPreview">
                    <span class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
                          :style="'background-image: url(\'' + idPreview + '\');'">
                    </span>
                </div>

                <jet-secondary-button class="mt-2 mr-2" type="button" @click.prevent="selectNewId">
                    Select A New ID
                </jet-secondary-button>

                <jet-secondary-button type="button" class="mt-2" @click.prevent="deleteId" v-if="user.id_path">
                    Remove ID
                </jet-secondary-button>

                <jet-input-error :message="form.errors.id" class="mt-2" />
            </div>
            <!-- Utility Bill -->
            <div class="col-span-6 sm:col-span-4">
                <!-- Utility Bill File Input -->
                <input type="file" class="hidden"
                            ref="bill"
                            @change="updateBillPreview">

                <jet-label for="bill" value="Utility Bill" />

                <!-- Current Utility Bill -->
                <div class="mt-2" v-show="! billPreview">
                    <img :src="user.utility_bill_url" :alt="user.first_name" class="rounded-full h-20 w-20 object-cover">
                </div>

                <!-- New Utility Bill Preview -->
                <div class="mt-2" v-show="billPreview">
                    <span class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
                          :style="'background-image: url(\'' + billPreview + '\');'">
                    </span>
                </div>

                <jet-secondary-button class="mt-2 mr-2" type="button" @click.prevent="selectNewBill">
                    Select A New Utility Bill
                </jet-secondary-button>

                <jet-secondary-button type="button" class="mt-2" @click.prevent="deleteBill" v-if="user.utility_bill_path">
                    Remove Utility Bill
                </jet-secondary-button>

                <jet-input-error :message="form.errors.bill" class="mt-2" />
            </div>
            <!-- Signature -->
            <div class="col-span-6 sm:col-span-4">
                <!-- Signature File Input -->
                <input type="file" class="hidden"
                            ref="signature"
                            @change="updateSignaturePreview">

                <jet-label for="signature" value="Signature" />

                <!-- Current Signature -->
                <div class="mt-2" v-show="! signaturePreview">
                    <img :src="user.signature_url" :alt="user.first_name" class="rounded-full h-20 w-20 object-cover">
                </div>

                <!-- New Signature Preview -->
                <div class="mt-2" v-show="signaturePreview">
                    <span class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
                          :style="'background-image: url(\'' + signaturePreview + '\');'">
                    </span>
                </div>

                <jet-secondary-button class="mt-2 mr-2" type="button" @click.prevent="selectNewSignature">
                    Select A Signature
                </jet-secondary-button>

                <jet-secondary-button type="button" class="mt-2" @click.prevent="deleteSignature" v-if="user.signature_path">
                    Remove Signature
                </jet-secondary-button>

                <jet-input-error :message="form.errors.signature" class="mt-2" />
            </div>
        </template>

        <template #actions>
            <jet-action-message :on="form.recentlySuccessful" class="mr-3">
                Verified!
            </jet-action-message>

            <jet-button :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
                Verify
            </jet-button>
        </template>
    </jet-form-section>
</template>

<script setup>
import { computed, ref } from 'vue'
import JetButton from '@/Jetstream/Button.vue'
import JetFormSection from '@/Jetstream/FormSection.vue'
import JetInput from '@/Jetstream/Input.vue'
import JetInputError from '@/Jetstream/InputError.vue'
import JetLabel from '@/Jetstream/Label.vue'
import JetActionMessage from '@/Jetstream/ActionMessage.vue'
import JetSecondaryButton from '@/Jetstream/SecondaryButton.vue'
import { useForm, usePage } from '@inertiajs/inertia-vue3'

defineProps({
    photoPreview: null,
    idPreview: null,
    billPreview: null,
    signaturePreview: null,
})

const photo = ref(null)
const id = ref(null)
const bill = ref(null)
const signature = ref(null)

const photoPreview = ref(null)
const idPreview = ref(null)
const billPreview = ref(null)
const signaturePreview = ref(null)

const user = computed(() => usePage().props.value.user)

const form = useForm({
    first_name: user.value.first_name,
    middle_name: user.value.middle_name,
    last_name: user.value.last_name,
    email: user.value.email,
    mobile_phone_number: user.value.mobile_phone_number,
    address: user.value.address,
    gender: '',
    religion: '',
    date_of_birth: '',
    marital_status: '',
    nationality: '',
    state_of_origin: '',
    local_govt_area: '',
    mother_maiden_name: '',
    next_of_kin_name: '',
    next_of_kin_relationship: '',
    next_of_kin_mobile_phone_number: '',
    next_of_kin_address: '',
    occupation: '',
    name_of_employer: '',
    employer_address: '',
    id_number: '',
    id_expiry_date: '',
    photo: null,
    id: null,
    bill: null,
    signature: null
})

function verifyIdentity() {
    if (photo.value) {
        form.photo = photo.value.files[0]
    }
    if (id.value) {
        form.id = id.value.files[0]
    }
    if (bill.value) {
        form.bill = bill.value.files[0]
    }
    if (signature.value) {
        form.signature = signature.value.files[0]
    }

    form.post(route('verify-identity'), {
        errorBag: 'verifyIdentity',
        preserveScroll: true,
        onSuccess: () => (clearPhotoFileInput(), clearIdFileInput(), clearBillFileInput(), clearSignatureFileInput()),
    });
}

function selectNewPhoto() {
    photo.value.click();
}

function updatePhotoPreview() {
    if (! photo.value.files[0]) return;
   
    const reader = new FileReader();

    reader.onload = (e) => {
        photoPreview.value = e.target.result;
    };

    reader.readAsDataURL(photo.value.files[0]);
}

function deletePhoto() {
    axios.delete(route('current-user-photo.destroy'))
    .then(res => {
        photoPreview.value = null; // removed preview still there until refresh
        (clearPhotoFileInput());
    })
}

function clearPhotoFileInput() {
    if (photo?.value) {
        photo.value = null;
    }
}

function selectNewId() {
    id.value.click();
}

function updateIdPreview() {
    if (! id.value.files[0]) return;
   
    const reader = new FileReader();

    reader.onload = (e) => {
        idPreview.value = e.target.result;
    };

    reader.readAsDataURL(id.value.files[0]);
}

function deleteId() {
    axios.delete(route('current-user-id.destroy'))
    .then(res => {
        idPreview.value = null; // removed preview still there until refresh
        (clearIdFileInput());
    })
}

function clearIdFileInput() {
    if (id?.value) {
        id.value = null;
    }
}

function selectNewBill() {
    bill.value.click();
}

function updateBillPreview() {
    if (! bill.value.files[0]) return;
   
    const reader = new FileReader();

    reader.onload = (e) => {
        billPreview.value = e.target.result;
    };

    reader.readAsDataURL(bill.value.files[0]);
}

function deleteBill() {
    axios.delete(route('current-user-bill.destroy'))
    .then(res => {
        billPreview.value = null; // removed preview still there until refresh
        (clearBillFileInput());
    })
}

function clearBillFileInput() {
    if (bill?.value) {
        bill.value = null;
    }
}

function selectNewSignature() {
    signature.value.click();
}

function updateSignaturePreview() {
    if (! signature.value.files[0]) return;
   
    const reader = new FileReader();

    reader.onload = (e) => {
        signaturePreview.value = e.target.result;
    };

    reader.readAsDataURL(signature.value.files[0]);
}

function deleteSignature() {
    axios.delete(route('current-user-signatute.destroy'))
    .then(res => {
        signaturePreview.value = null; // removed preview still there until refresh
        (clearSignatureFileInput());
    })
}

function clearSignatureFileInput() {
    if (signature?.value) {
        signature.value = null;
    }
}
</script>

0 Answers
Related