shadcn-vue
shadcn-vue copied to clipboard
[Bug]: Multistep form is not hydrating Step 2 inputs
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