Pass data props from child component to parent in Vue3

64 views Asked by At

i'm writing a personal project for my portfolio. I'm making a registration site.

So the user will fill: personal data, shipping info and then will see a recap.

I have a view (Register.vue) where I put a stepper from Vuetify to make things faster.

There are three steps: PersonalDataForm (a separated component), ShippingForm (another separated component) and recap.

So as for every step the user can fill the form and then proceed to the next step.

The problems I am facing are:

  1. I want to make the "next" button disabled unless the form is completed and i don't know how to deal with the vuetify stepper
  2. I don't know how to pass props so i can't make the first problem solvable.

REGISTER VIEW

<template>
  <v-stepper color="deep-purple-darken-1" :items="['Dati Anagrafici', 'Indirizzo di spedizione', 'Riepilogo']" class="my-5">
    <template v-slot:item.1>
      <v-card>
        <PersonalDataForm @formDataSubmitted="handlePersonalDataForm" :isFormIncomplete="personalDataFormIsIncomplete"/>
      </v-card>
    </template>

    <template v-slot:item.2>
      <v-card>
        <ShippingForm @formDataSubmitted="handleShippingForm" :isFormIncomplete="shippingFormIsIncomplete"/>
      </v-card>
    </template>

    <template v-slot:item.3>
      <v-card>
        <Recap @completed="submitData" :disabled="isFormIncomplete"/>
      </v-card>
    </template>
  </v-stepper>
</template>

<script setup>
import { ref, computed } from 'vue';
import PersonalDataForm from '@/components/forms/PersonalDataForm.vue';
import ShippingForm from '@/components/forms/ShippingForm.vue';
import Recap from '../components/Recap.vue';

const personalDataFormIsIncomplete = ref(true);
const shippingFormIsIncomplete = ref(true);

const isFormIncomplete = computed(() => {
  return personalDataFormIsIncomplete.value || shippingFormIsIncomplete.value;
});

const handlePersonalDataForm = (formData) => {
  console.log("Received personal data:", formData);
  personalDataFormIsIncomplete.value = false;
};

const handleShippingForm = (formData) => {
  console.log("Received shipping data:", formData);
  shippingFormIsIncomplete.value = false;
};

const submitData = () => {
  console.log('Submitting data...');
};
</script>
```

PersonalDataForm.vue
```
<template>
  <v-container>
    <v-card class="rounded-lg py-4 px-8" elevation="8">
      <form @submit.prevent="handleSubmit">
        <v-row>
          <v-col cols="12" md="6">
            <v-text-field
              v-model="state.firstName"
              :error-messages="v$.firstName.$errors.map((e) => e.$message)"
              label="Nome"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.firstName.$touch"
              @input="v$.firstName.$touch"
            ></v-text-field>
          </v-col>

          <v-col cols="12" md="6">
            <v-text-field
              v-model="state.lastName"
              :error-messages="v$.lastName.$errors.map((e) => e.$message)"
              label="Cognome"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.lastName.$touch"
              @input="v$.lastName.$touch"
            ></v-text-field>
          </v-col>

          <v-col cols="12" md="6">
            <v-text-field
              v-model="state.birthDate"
              :error-messages="v$.birthDate.$errors.map((e) => e.$message)"
              type="date"
              label="Data di Nascita"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.birthDate.$touch"
              @input="v$.birthDate.$touch"
            ></v-text-field>
          </v-col>

          <v-col cols="12" md="6">
            <v-text-field
              v-model="state.birthPlace"
              :error-messages="v$.birthPlace.$errors.map((e) => e.$message)"
              label="Città di Nascita"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.birthPlace.$touch"
              @input="v$.birthPlace.$touch"
            ></v-text-field>
          </v-col>

          <v-col cols="12" md="6">
            <v-text-field
              v-model="state.fiscalCode"
              :error-messages="v$.fiscalCode.$errors.map((e) => e.$message)"
              label="Codice Fiscale"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.fiscalCode.$touch"
              @input="v$.fiscalCode.$touch"
            ></v-text-field>
          </v-col>
        </v-row>
        <v-btn class="me-4" @click="handleSubmit"> submit </v-btn>

      </form>
    </v-card>
  </v-container>
</template>

<script setup>
import { ref, computed } from 'vue';
import { useVuelidate } from "@vuelidate/core";
import { required } from "@vuelidate/validators";

const state = ref({
  firstName: "",
  lastName: "",
  birthDate: "",
  birthPlace: "",
  fiscalCode: "",
});

const rules = {
  firstName: { required },
  lastName: { required },
  birthDate: { required },
  birthPlace: { required },
  fiscalCode: { required }
};

const v$ = useVuelidate(rules, state.value);

const isFormIncomplete = computed(() => {
  return Object.values(state.value).some(value => !value);
});

function handleSubmit() {
  v$.value.$touch();

  if (!v$.value.$invalid) {
    // Form is valid, proceed with submitting data
    const formData = { ...state.value };
    console.log("Submitting form data:", formData);

    // Here you can call your submit function or perform any necessary action
    // Example: submitFormData(formData);
  } else {
    // Form is invalid, do something (e.g., show error message)
    console.log("Form has validation errors, cannot submit.");
  }
}
</script>
```

ShippingForm.vue
<template>
  <v-container>
    <v-card class="rounded-lg py-4 px-8" elevation="8">
      <form @submit.prevent="handleSubmit">
        <v-row>
          <!-- fare che tipo sia precompilato e non modificabile -->
          <v-col cols="12" md="6">
            <v-text-field
              v-model="state.firstName"
              :error-messages="v$.firstName.$errors.map((e) => e.$message)"
              label="Nome"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.firstName.$touch"
              @input="v$.firstName.$touch"
            ></v-text-field>
          </v-col>

          <!-- fare che tipo sia precompilato e non modificabile -->
          <v-col cols="12" md="6">
            <v-text-field
              v-model="state.lastName"
              :error-messages="v$.lastName.$errors.map((e) => e.$message)"
              label="Cognome"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.lastName.$touch"
              @input="v$.lastName.$touch"
            ></v-text-field>
          </v-col>

          <v-col cols="12" md="6">
            <v-select
              v-model="state.region"
              :items="regions"
              :error-messages="v$.region.$errors.map((e) => e.$message)"
              label="Regione"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.region.$touch"
              @input="v$.region.$touch"
            ></v-select>
          </v-col>

          <v-col cols="12" md="6">
            <v-select
              v-model="state.province"
              :items="provinces"
              :error-messages="v$.province.$errors.map((e) => e.$message)"
              label="Provincia"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.province.$touch"
              @input="v$.province.$touch"
            ></v-select>
          </v-col>

          <v-col cols="12" md="6">
            <v-select
              v-model="state.city"
              :items="cities"
              :error-messages="v$.city.$errors.map((e) => e.$message)"
              label="Città"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.city.$touch"
              @input="v$.city.$touch"
            ></v-select>
          </v-col>

          <v-col cols="12" md="6">
            <v-text-field
              v-model="state.houseNumber"
              :error-messages="v$.houseNumber.$errors.map((e) => e.$message)"
              label="Civico"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.houseNumber.$touch"
              @input="v$.houseNumber.$touch"
            ></v-text-field>
          </v-col>

          <v-col cols="12" md="6">
            <v-text-field
              v-model="state.email"
              :error-messages="v$.email.$errors.map((e) => e.$message)"
              label="E-mail"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.email.$touch"
              @input="v$.email.$touch"
            ></v-text-field>
          </v-col>

          <v-col cols="12" md="6">
            <v-text-field
              v-model="state.phoneNumber"
              :error-messages="v$.phoneNumber.$errors.map((e) => e.$message)"
              label="Telefono"
              required
              variant="underlined"
              color="deep-purple-lighten-1"
              @blur="v$.phoneNumber.$touch"
              @input="v$.phoneNumber.$touch"
            ></v-text-field>
          </v-col>
        </v-row>
        <v-btn class="me-4" type="submit" @click="handleSubmit"> submit </v-btn>
      </form>
    </v-card>
  </v-container>
</template>

<script setup>
import { reactive, computed, defineProps } from 'vue';
import { useVuelidate } from "@vuelidate/core";
import {
  email,
  required,
  numeric,
  minLength,
  maxLength,
} from "@vuelidate/validators";

const props = defineProps(['formData']);
const emit = props['onUpdate:formData'];

const initialState = {
  firstName: "",
  lastName: "",
  region: "",
  province: "",
  city: "",
  houseNumber: "",
  email: "",
  phoneNumber: "",
};

const state = reactive({
  ...initialState,
});

const isFormIncomplete = computed(() => {
  return Object.values(state).some(value => !value); // Check if any field is empty
});


const regions = ["Emilia", "Lombardia", "Calabria", "Piemonte"];

const provinces = ["piacenza", "Milano", "Cosenza", "Torino"];

const cities = ["Caorso", "Assago", "Lamezia", "Settimo Torinese"];

const rules = {
  firstName: { required },
  lastName: { required },
  region: { required },
  province: { required },
  city: { required },
  houseNumber: { required, numeric },
  email: { required, email },
  phoneNumber: {
    required,
    numeric,
    minLength: minLength(12),
    maxLength: maxLength(12),
  },
};

const v$ = useVuelidate(rules, state);

const getData = computed(() => {
  return {
    firstName: state.firstName,
    lastName: state.lastName,
    region: state.region,
    province: state.province,
    city: state.city,
    houseNumber: state.houseNumber,
    email: state.email,
    phoneNumber: state.phoneNumber,
  };
});


function handleSubmit() {
  v$.value.$touch();
  emit('formDataSubmitted', formData.value);

  if (!v$.value.$invalid) {
    // Form is valid, proceed with submitting data
    const formData = { ...state };
    console.log("Submitting form data:", formData);

    // Here you can call your submit function or perform any necessary action
    // Example: submitFormData(formData);
  } else {
    // Form is invalid, do something (e.g., show error message)
    console.log("Form has validation errors, cannot submit.");
  }
}
</script>
`
2

There are 2 answers

1
Maik Lowrey On BEST ANSWER

If you have a simple and direct parent-child relationship/structure, emit is your friend. If you have nested structures, emit is awkward. Because you have to work your way up through any hierarchy. This is where pinia can help. Pinia is a storage. Here you define actions and can access them globally. This simplifies life enormously and makes you independent of parent-child structures.

https://pinia.vuejs.org

0
mahbuburrahman rifat On

Simple Solution would be have boolean flags for each form type. If a form submission is valid update the correponsding boolean flag and show/disable the next button.

You need to create the formDataSubmitted event and emit this event on successful form submission. Listen for this event in the parent component and update the boolean flag.

<script setup>
const emit = defineEmits(['formDataSubmitted']);

function handleSubmit() {
  ...other code

  if(formValid) {
     emit('formDataSubmitted', formData);
  }
}
</script>

Learn more about how events work in Vue here: Vue Event Emitter