shadcn-vue icon indicating copy to clipboard operation
shadcn-vue copied to clipboard

[Bug]: Multistep form is not hydrating Step 2 inputs

Open julienguillot77 opened this issue 1 year ago • 5 comments

Reproduction

Look at Description

Describe the bug

I followed the code as written on this page :

https://www.shadcn-vue.com/docs/components/stepper.html#form

The only difference is that I'm hydrating the form with data coming from the api with useAsyncData's data ref type.

Step 1 inputs are well hydrated but not Step 2 one.

Here is my code :

<template>
  <Head>
    <Title>{{ $i18n.t("pages.activation.title") }}</Title>
  </Head>

  <div v-if="data">
    <Form
      v-slot="{ meta, values, validate }"
      keep-values
      :validation-schema="toTypedSchema(formSchema[stepIndex - 1])"
      :initial-values="data"
    >
      <Stepper
        v-slot="{ isNextDisabled, isPrevDisabled, nextStep, prevStep }"
        v-model="stepIndex"
        class="block w-full"
      >
        <form
          @submit="
            (e) => {
              e.preventDefault();
              validate();

              if (stepIndex === steps.length && meta.valid) {
                onSubmit(values);
              }
            }
          "
        >
          <div
            class="flex flex-start gap-2 lg:w-1/3 sm:w-full xs:mx-2 sm:mx-auto my-6"
          >
            <StepperItem
              v-for="step in steps"
              :key="step.step"
              v-slot="{ state }"
              class="relative flex w-full flex-col items-center justify-start"
              :step="step.step"
            >
              <StepperSeparator
                v-if="step.step !== steps[steps.length - 1].step"
                class="absolute left-[calc(50%+20px)] right-[calc(-50%+10px)] top-5 block h-0.5 shrink-0 rounded-full bg-muted group-data-[state=completed]:bg-primary"
              />

              <StepperTrigger as-child>
                <Button
                  :variant="
                    state === 'completed' || state === 'active'
                      ? 'default'
                      : 'outline'
                  "
                  size="icon"
                  class="z-10 rounded-full shrink-0"
                  :class="[
                    state === 'active' &&
                      'ring-2 ring-ring ring-offset-2 ring-offset-background',
                  ]"
                  :disabled="state !== 'completed' && !meta.valid"
                >
                  <Check v-if="state === 'completed'" class="size-5" />
                  <Circle v-if="state === 'active'" />
                  <Dot v-if="state === 'inactive'" />
                </Button>
              </StepperTrigger>

              <div class="mt-5 flex flex-col items-center text-center">
                <StepperTitle
                  :class="[state === 'active' && 'text-primary']"
                  class="text-sm font-semibold transition lg:text-base"
                >
                  {{ step.title }}
                </StepperTitle>
                <StepperDescription
                  :class="[state === 'active' && 'text-primary']"
                  class="sr-only text-xs text-muted-foreground transition md:not-sr-only lg:text-sm"
                >
                  {{ step.description }}
                </StepperDescription>
              </div>
            </StepperItem>
          </div>

          <Card class="lg:w-1/3 sm:w-full xs:mx-2 sm:mx-auto">
            <CardContent>
              <div class="flex flex-col gap-4 mt-4">
                <template v-if="stepIndex === 1">
                  <FormField v-slot="{ componentField }" name="last_name">
                    <FormItem>
                      <FormLabel>{{
                        $i18n.t("models.admin_registration_form.last_name")
                      }}</FormLabel>
                      <FormControl>
                        <Input type="text" v-bind="componentField" />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  </FormField>

                  <FormField v-slot="{ componentField }" name="first_name">
                    <FormItem>
                      <FormLabel>{{
                        $i18n.t("models.admin_registration_form.first_name")
                      }}</FormLabel>
                      <FormControl>
                        <Input type="text" v-bind="componentField" />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  </FormField>
                </template>

                <template v-else-if="stepIndex === 2">
                  <FormField v-slot="{ componentField }" name="username">
                    <FormItem>
                      <FormLabel>{{
                        $i18n.t("models.admin_registration_form.username")
                      }}</FormLabel>
                      <FormControl>
                        <Input
                          type="text"
                          v-bind="componentField"
                          autocomplete="username"
                        />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  </FormField>

                  <FormField v-slot="{ componentField }" name="email">
                    <FormItem>
                      <FormLabel>{{
                        $i18n.t("models.admin_registration_form.email")
                      }}</FormLabel>
                      <FormControl>
                        <Input
                          type="email"
                          v-bind="componentField"
                          autocomplete="email"
                        />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  </FormField>

                  <FormField v-slot="{ componentField }" name="password">
                    <FormItem>
                      <FormLabel>{{
                        $i18n.t("models.admin_registration_form.password")
                      }}</FormLabel>
                      <FormControl>
                        <Input
                          type="password"
                          v-bind="componentField"
                          autocomplete="new-password"
                        />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  </FormField>

                  <FormField
                    v-slot="{ componentField }"
                    name="password_confirmation"
                  >
                    <FormItem>
                      <FormLabel>{{
                        $i18n.t(
                          "models.admin_registration_form.password_confirmation"
                        )
                      }}</FormLabel>
                      <FormControl>
                        <Input
                          type="password"
                          v-bind="componentField"
                          autocomplete="new-password"
                        />
                      </FormControl>
                      <FormMessage />
                    </FormItem>
                  </FormField>
                </template>

                <template v-else-if="stepIndex === 3">
                  <div>
                    <Table>
                      <TableBody>
                        <TableRow>
                          <TableCell>{{
                            $i18n.t("models.admin_registration_form.full_name")
                          }}</TableCell>
                          <TableCell>
                            {{ values.last_name }} {{ values.first_name }}
                          </TableCell>
                        </TableRow>
                        <TableRow>
                          <TableCell>{{
                            $i18n.t("models.admin_registration_form.username")
                          }}</TableCell>
                          <TableCell>
                            {{ values.username }}
                          </TableCell>
                        </TableRow>
                        <TableRow>
                          <TableCell>{{
                            $i18n.t("models.admin_registration_form.email")
                          }}</TableCell>
                          <TableCell>
                            {{ values.email }}
                          </TableCell>
                        </TableRow>
                        <TableRow>
                          <TableCell>{{
                            $i18n.t("models.admin_registration_form.role")
                          }}</TableCell>
                          <TableCell>
                            {{ data.role }}
                          </TableCell>
                        </TableRow>
                        <TableRow v-if="values.role == 'store_manager'">
                          <TableCell>{{
                            $i18n.t("models.admin_registration_form.store")
                          }}</TableCell>
                          <TableCell>
                            {{ data.store_id }}
                          </TableCell>
                        </TableRow>
                      </TableBody>
                    </Table>
                  </div>
                </template>
              </div>
            </CardContent>
          </Card>

          <div
            class="flex items-center justify-between mt-4 lg:w-1/3 sm:w-full xs:mx-2 sm:mx-auto"
          >
            <Button
              :disabled="isPrevDisabled"
              variant="outline"
              size="sm"
              @click="prevStep()"
            >
              <Icon name="ic:chevron-left" />
            </Button>
            <div class="flex items-center gap-3">
              <Button
                v-if="stepIndex !== 3"
                :type="meta.valid ? 'button' : 'submit'"
                :disabled="isNextDisabled"
                size="sm"
                @click="meta.valid && nextStep()"
              >
                <Icon name="ic:chevron-right" />
              </Button>
              <Button v-if="stepIndex === 3" size="sm" type="submit">
                {{ $i18n.t("common.finish") }}
              </Button>
            </div>
          </div>
        </form>
      </Stepper>
    </Form>
  </div>
  <div v-else>ERROR</div>
</template>

<script lang="ts" setup>
import { toast } from "~/components/ui/toast";
import { Check, Circle, Dot } from "lucide-vue-next";
import { toTypedSchema } from "@vee-validate/zod";
import * as z from "zod";

definePageMeta({
  auth: { unauthenticatedOnly: true, navigateAuthenticatedTo: "/" },
  layout: false,
});

const route = useRoute();
const router = useRouter();
const { $i18n } = useNuxtApp();
const config = useRuntimeConfig();

const queryParams = route.query;
const token = queryParams.token;

const stepIndex = ref(1);
const steps = [
  {
    step: 1,
    title: $i18n.t("pages.activation.steps.step1.title"),
    description: $i18n.t("pages.activation.steps.step1.description"),
  },
  {
    step: 2,
    title: $i18n.t("pages.activation.steps.step2.title"),
    description: $i18n.t("pages.activation.steps.step2.description"),
  },
  {
    step: 3,
    title: $i18n.t("pages.activation.steps.step3.title"),
    description: $i18n.t("pages.activation.steps.step3.description"),
  },
];

if (!token) {
  router.push({ path: "/auth/login" });

  toast({
    duration: 3000,
    variant: "error",
    description: "No activation token provided.",
  });
}

const formSchema: any = [
  // Step 1
  z.object({
    first_name: z.string().min(1),
    last_name: z.string().min(1),
  }),
  // Step 2
  z
    .object({
      username: z.string().min(1),
      email: z.string().email(),
      password: z.string().min(8),
      password_confirmation: z.string().min(8),
    })
    .refine(
      (values) => {
        return values.password === values.password_confirmation;
      },
      {
        message: "Passwords must match!",
        path: ["password_confirmation"],
      }
    ),
];

const { data, status, error } = useAsyncData<any>("activation", () =>
  $fetch(
    `${config.public.apiEndpointUrl}management/admins/activate?token=${token}`
  )
);

function onSubmit(values: any) {
  toast({
    title: "You submitted the following values:",
    description: h(
      "pre",
      { class: "mt-2 w-[340px] rounded-md bg-slate-950 p-4" },
      h("code", { class: "text-white" }, JSON.stringify(values, null, 2))
    ),
  });
}

watchEffect(() => {
  if (error.value) {
    if (error.value.statusCode === 404) {
      router.push({ path: "/auth/login" });

      toast({
        duration: 3000,
        variant: "error",
        description: "Invalid or expired activation token.",
      });
    } else {
      toast({
        duration: 3000,
        variant: "error",
        description: "An error occurred while fetching data.",
      });
    }
  }
});
</script>

<style></style>

I've searched on the web the reason but not found anything interesting.

System Info

System:
    OS: macOS 15.0.1
    CPU: (10) arm64 Apple M1 Max
    Memory: 207.83 MB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.17.0 - ~/.nvm/versions/node/v20.17.0/bin/node
    Yarn: 1.22.22 - ~/.nvm/versions/node/v20.17.0/bin/yarn
    npm: 10.8.3 - ~/.nvm/versions/node/v20.17.0/bin/npm
  Browsers:
    Brave Browser: 130.1.71.118
    Chrome: 130.0.6723.92
    Safari: 18.0.1
  npmPackages:
    @vueuse/core: ^11.1.0 => 11.1.0 
    nuxt: ^3.13.2 => 3.13.2 
    radix-vue: ^1.9.6 => 1.9.7 
    shadcn-nuxt: ^0.10.4 => 0.10.4 
    vue: 3.5.12 => 3.5.12

Contributes

  • [ ] I am willing to submit a PR to fix this issue
  • [ ] I am willing to submit a PR with failing tests

julienguillot77 avatar Nov 01 '24 02:11 julienguillot77