typescriptvue.jsvue-componentzod

How to set a select options in Shadcn-vue AutoForm with Zod schema?


I'm using ShadCN Vue with AutoForm and zod for schema validation. I'm trying to dynamically populate the select options for a field named accounts using an array of account objects.

My code:

<script setup lang="ts">
import { Button } from '#/components/ui/button';
import AutoForm from '#/components/ui/auto-form/AutoForm.vue';
import { z } from 'zod';
import { computed } from 'vue';

type Account = {
  id?: number;
  serviceId: number;
  email: string;
  password: string;
  startDate: string;
  endDate: string;
  cost: number;
};
type Service = {
  id?: number;
  name: string;
  pricePerMonth: number;
  maxUsers: number;
  accounts?: Account[];
};

// Fake data from DB
const allAccounts: Account[] = [
  {
    id: 1,
    serviceId: 1, // fk
    email: 'test@test.com',
    password: '123456',
    startDate: new Date(),
    endDate: new Date(),
    cost: 100,
  },
];
const services: Service[] = [
  {
    id: 1,
    name: 's 1',
    pricePerMonth: 100,
    maxUsers: 10,
    accounts: [allAccounts[0]],
  },
];

// The schema
const serviceSchema = z.object({
  name: z
    .string()
    .min(1, 'Name is required')
    .optional()
    .default(services[0]?.name || ''),
  pricePerMonth: z
    .number()
    .min(0, 'Price must be at least 0')
    .max(10000, 'Price cannot exceed 10000')
    .optional()
    .default(
      z
        .preprocess((x) => Number(x), z.number())
        .parse(services[0]?.pricePerMonth || 0)
    ),
  maxUsers: z
    .number()
    .min(1, 'Max users must be at least 1')
    .max(1000, 'Max users cannot exceed 1000')
    .optional()
    .default(services[0]?.maxUsers || 1),
  accounts: z
    .array(
      z.object({
        id: z.string(),
        email: z.string(),
      })
    )
    .default(
      allAccounts.map((account: Account) => ({
        id: String(account.id),
        email: account.email,
      }))
    ),
});

const fkIds = computed(() =>
  allAccounts.map((account) => ({
    value: String(account.id),
    label: account.email,
  }))
);
</script>

<template>
  fkIds: {{ fkIds }}
  <AutoForm
    class="w-2/3 mx-auto mt-20 space-y-6"
    :schema="serviceSchema"
    @submit="onSubmit"
    :field-config="{
      accounts: {
        component: 'select',
        options: fkIds
          ? Object.fromEntries(fkIds.map((item) => [item.value, item.label]))
          : [],
      },
    }"
  >
    <div>
      <Button type="submit"> Update </Button>
    </div>
  </AutoForm>
</template>

Here is a live demo of what I tried https://stackblitz.com/edit/vue-shadcn-template-58gk2vtp?file=src%2FApp.vue

I want the accounts field to render as a <select> with dynamic options from fkIds, but nothing shows up in the UI. I'm not sure if I'm binding the options correctly for ShadCN Vue’s AutoForm.

a screenshot for the current result

I want to properly render a select input for the accounts field and to populate it dynamically using the provided fkIds


Solution

  • Replace the accounts array with a single enum field like

    accounts: z.enum(allAccounts.map(acc => String(acc.id))) 
    

    Remove or rename "<template #accounts>" as it's overriding the default field rendering.

    <script setup lang="ts">
    import { Button } from '#/components/ui/button';
    import AutoForm from '#/components/ui/auto-form/AutoForm.vue';
    import { z } from 'zod';
    import { computed } from 'vue';
    
    type Account = {
      id?: number;
      serviceId: number;
      email: string;
      password: string;
      startDate: string;
      endDate: string;
      cost: number;
    };
    type Service = {
      id?: number;
      name: string;
      pricePerMonth: number;
      maxUsers: number;
      accounts?: Account[];
    };
    
    // Fake data from DB
    const allAccounts: Account[] = [
      {
        id: 1,
        serviceId: 1,
        email: 'test1@test.com',
        password: '123456',
        startDate: new Date(),
        endDate: new Date(),
        cost: 100,
      },
      {
        id: 2,
        serviceId: 1,
        email: 'test2@test.com',
        password: '654321',
        startDate: new Date(),
        endDate: new Date(),
        cost: 120,
      },
    ];
    const services: Service[] = [
      {
        id: 1,
        name: 's 1',
        pricePerMonth: 100,
        maxUsers: 10,
        accounts: allAccounts,
      },
    ];
    
    // The schema
    const serviceSchema = z.object({
      name: z
        .string()
        .min(1, 'Name is required')
        .optional()
        .default(services[0]?.name || ''),
      pricePerMonth: z
        .number()
        .min(0, 'Price must be at least 0')
        .max(10000, 'Price cannot exceed 10000')
        .optional()
        .default(
          z
            .preprocess((x) => Number(x), z.number())
            .parse(services[0]?.pricePerMonth || 0)
        ),
      maxUsers: z
        .number()
        .min(1, 'Max users must be at least 1')
        .max(1000, 'Max users cannot exceed 1000')
        .optional()
        .default(services[0]?.maxUsers || 1),
      accounts: z.enum(allAccounts.map((acc) => String(acc.id))),
    });
    
    const fkIds = computed(() =>
      allAccounts.map((account) => ({
        value: String(account.id),
        label: account.email,
      }))
    );
    </script>
    
    <template>
      fkIds: {{ fkIds }}
      <br />
      s: {{ Object.fromEntries(fkIds.map((item) => [item.value, item.label])) }}
    
      <AutoForm
        :schema="serviceSchema"
        @submit="onSubmit"
        class="w-2/3 mx-auto space-y-6"
      >
        <Button type="submit">Update</Button>
      </AutoForm>
    </template>