1

I'm working in Nuxt3 and I've got a slightly unusual setup trying to watch or retrieve data from child components in a complex form that is structured as a multi-step wizard. It's obviously Vue underneath and I'm using the composition API.

My setup is that I have a page the wizard component is on, and that component has a prop that is an array of steps in the wizard. Each of these steps is some string fields for titles and labels and then a component type for the content. This way I can reuse existing form blocks in different ways. The key thing to understand is that the array of steps can be any length and contain any type of component.

Ideally, I'd like each child component to be unaware of being in the wizard so it can be reused elsewhere in the app. For example, a form that is one of the steps should handle its own validation and make public its state in a way the wizard component can read or watch.

The image below explains my basic setup.

                             enter image description here

The page includes this tag:

<Wizard :steps="steps" :object="project" @submit="createProject"/>

The Wizard loops over the steps to create each component.

<div v-for="(step) in steps" :key="step.name">
  <component v-if="step.status === 'current'" :is="step.content.component" />
</div>

The data to setup the component with the right props for the wizard itself and the child component props.

const steps = ref([
{ 
    name: 'overview',
    title: t('overview'),
    subTitle: t('projectCreateOverviewDescription'),
    status: 'current',
    invalid: true,
    content: {
        component: Overview,
        props: null,
        model: {}
    }
},
{ 
    name: 'members',
    title: t('members'),
    subTitle: t('projectCreateMembersDescription'),
    status: 'upcoming',
    invalid: false,
    content: {
        component: ThumbnailList,
        props: {
            objects: users,
            title: t('users'),
            objectNameSingular: t('user'),
            objectNamePlural: t('users'),

So far I've tried to dynamically create references in the wizard component to watch the state of the children but those refs are always null. This concept of a null ref seems to be the accepted answer elsewhere when binding to known child components, but with this dynamic setup, it doesn't seem to be the right route.

interface StepRefs {
   [key: string]: any
}

let stepRefs: StepRefs = {}

props.steps.forEach(step => {
    stepRefs[step.name] = ref(null)

    watch(() => stepRefs[step.name].value, (newValue, oldValue) => {
        console.log(newValue)
        console.log(oldValue)
    }, { deep: true })
})

Can anyone direct me to the right approach to take for this setup? I have a lot of these wizards in different places in the app so a component approach is really attractive, but if it comes to it I'll abandon the idea and move that layer of logic to the pages to avoid the dynamic aspect.

2 Answers 2

2

To handle changes in child components I'd recommend to use events. You can have the children emit an event on change or completion, and the wizard is listening to events from all children and handling them respectively.

On the wizard subscribe to the event handler of the step component, and process the data coming from each step on completion (or whatever stage you need).

This way you don't need any special data type for the steps, they can just be an array. Simply use a ref to keep track of the current step. You don't even need a v-for, if you just display one step at a time. For a wizard navigation you might still need a v-for, but it would be much simpler. Please see a rough example below.

<div>
 <stepComponent step="currentStep" @step-complete="handleStepComplete"/>
 <div>
  <wizardNavigationItemComponent v-for="step in steps" :active="step.name === currentStep.name" />
 </div>
</div>

<script setup lang="ts">

 const steps = step[/*your step data here*/]

 const currentStepIndex = ref(0)
 const currentStep = ref(steps[currentStepIndex.value])

 function handleStepComplete(data) {
  /* handle the data and move to next step */
  currentStepIndex.value =  currentStepIndex.value + 1 % steps.length
 }

</script>

In the component you just need to define the event and emit it when the data is ready, to pass along the data:

<script setup lang="ts">

const emit = defineEmits<{
  (event: "stepComplete", data: <your data type>): void
}>()

/* call emit in the component when its done / filled */
emit("stepComplete", data)

</script>

I hope this helps and can provide a viable path forward for you!

Sign up to request clarification or add additional context in comments.

Comments

0

Not sure if Mark found a solution but I had the same issue and this is what I did to solve it. I am new to composition API so may not be orthodox but it does what I want to do and solves the issue above. I needed to call a function that validated the form data then updated the store if valid. I first tried to set ref as an object and use :ref="steps" but was too difficult to get the reference to the step so I used a function to create an associative array/object using the tab component name as a key so it was simple to access the reference by component name.

First create an object to store the component elements: const steps = ref({})

When the component is loaded use :rel to call a function "addStepRef" to add the element and tab reference to steps object. Step reference is therefore steps[tab] and just like that I can access the child components exposed elements. Note, I also load the component dynamically using :is. Possibly could have combined the functions but I prefer using both :rel and :is for readability.

<script setup >
//local registration
import { FormWizard, TabContent } from "vue3-form-wizard";
import {ref, defineAsyncComponent, onMounted } from "vue";

const steps = ref({})

const tabs = [
    { "title": "Product", "icon": "ti-user", "component": "Product", "route": "", "hide": false },
    { "title": "Options", "icon": "ti-settings", "component": "Options", "route": "", "hide": false},
    { "title": "Review", "icon": "ti-settings", "component": "Review", "route": "", "hide": false }
]

const addStepRef = (tab) => el => {
    if (el) {
        steps[tab] = el
    }
}
    
// default form if tab form is not found
import Default from "@/forms/components/tabs/Default.vue";
// Loads the tabs for the form wizard
const getComponent = (tabname) => defineAsyncComponent(() =>
    import(`./components/tabs/${tabname}.vue`).catch(() => Default)
);

const validate = (tab) => {

    let result = true
    let step = steps[tab]

    if(typeof step.validate === 'function')
    {
        result = step.validate();
    }  
    return result
}

const onComplete = ()=>{
    alert("Yay. Done!");
};

</script>

<template>

    <FormWizard  @on-complete="onComplete" ref="wizard">

        <tab-content v-for="tab in tabs"
                     :key="tab.title"
                     :title="tab.title"
                     :icon="tab.icon"
                     :before-change="() => validate(tab.component)"
        >
            <component :ref="addStepRef(tab.component)" :is="getComponent(tab.component)"></component>

        </tab-content>

    </FormWizard>
</template>

Remember to expose the functions (eg. validate) you need in your child components. Took me a few days to figure all this out so hope this helps others.

Form wizard is based on: https://vue3-form-wizard-document.netlify.app/

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.