radix-vue / shadcn-vue

Vue port of shadcn-ui
https://www.shadcn-vue.com/
MIT License
4.55k stars 261 forks source link

[Feature]: AutoForm: more flexibility #590

Open razbakov opened 3 months ago

razbakov commented 3 months ago

Describe the feature

Background

In my vue2 project I used form builder based on json array of fields.

I was looking for similar solution for vue3 projects. I found https://formkit.com/ and it seemed like a good choice in the beginning, but then I encountered:

  1. Styling problems.
  2. Basic components only available in Pro: Repeater, Toggle, Autocomplete, Datepicker, Dropdown.
  3. Creating a custom input is too complex.

I am trying to think of some solution that is simple and flexible. For a long time I couldn't decide how to build forms: with js config or with template. With template you can define a custom layout and place multiple inputs (for example "first name" and "last name") in one row. On other hand you can re-use js config in multiple places.

I discovered Building forms with VeeValidate and Zod and I love how Zod makes life easier with validation and types.

When I integrated Form in my new project, I created form builder based on zod schema.

Recently I discovered AutoForm, I love the idea! It's very similar to what I was doing! Amazing!

Problem

Here is example of registration form with AutoForm.

Customization

What I am missing here is:

Configuration

And a smaller issue is 3 configs for one form: schema, field-config and slots. It would be awesome to have a single configuration. zod doesn't let add any additional information in schema, so I was thinking on creating a new type that would allow to integrate zod objects inside of it, for example:

const form = model({
  username: {
    label: "Username",
    component: "InputUsername",
    type: z
      .string()
      .min(2, "Username must be at least 2 characters.")
      .max(30)
      .refine(
        noMultiplePeriods,
        "Username cannot have multiple periods in a row."
      )
      .refine(notEndingInPeriod, "Username cannot end in a period.")
      .refine(usernameValidator, "Username is already taken."),
  },
  city: z.string(),
  email: z.string().email().default("me@example.com"),
  password: z.string().min(8),
  acceptTerms: z.boolean().refine((value) => value, {
    message: "You must accept the terms and conditions.",
  }),
});

const schema = form.schema(); // returns z.object

Additional information

Nyantekyi commented 2 months ago

Hello... so I have this problem I am trying to use auto forms to solve... I dont know if you can help. In my project I have a profile page where users are expected to provide their current location... and other stuff. This is the zod schema.

export const userprofile=z.object({
    username: z.string().min(1).max(150),
    first_name:z.string().max(150).min(1),
    last_name:z.string().max(150).min(1),
    email: z.string().email(),
})

export const phone=z.object({
    code:z.number(),
    phone: z.string().min(9).max(10)
})

export const address=z.object({
    zip:z.string().max(6).nullish(),
    zip4:z.string().max(4).nullish(),
    line:z.string().min(1).max(50),
    line2:z.string().max(50),
     lat:z.number().min(-90).max(90).nullish(),
     lon: z.number().min(-180).max(180).nullish(),
     city:z.number().min(1)
})

export const profileforms=z.object({
    user:userprofile,
    picture:z.any().refine((file) => file?.size <= MAX_FILE_SIZE, `Max image size is 5MB.`).refine((file) => ACCEPTED_IMAGE_TYPES.includes(file?.type),"Only .jpg, .jpeg, .png and .webp formats are supported."),
    bio:z.string().max(500),
    birth_date:z.string().date(),
    marital_status:z.enum(['Married', 'Divorce', 'Single',]),
    occupation: z.array(z.string().uuid()),
    address:address,
    phone:z.array(phone), 
})

This is the schema the data base experts from the client. The database provides a list of occupations(pk-uuid),phone code(number), city codes(number)... How do I use auto forms in this case while maintaining the validity of the data... For instance... I want the users to have the freedom of picking any city... in say a combobox(with an api filter) and then only pick the id of the selected value. And also how do I change the nested pattern of the user(in profile) and address(in profile) from an accordion to a group

Nyantekyi commented 2 months ago

@razbakov if you could help....ow and I also noticed when using vee validate... and I want to validate some fields directly... like this (here I am trying to ensure that the user name in the sign up form does not already exist in the database... the error is not persisted... it appears and the disappears as soon as the cell is no longer in focus

     <template #username="slotProps">
                <AutoFormField v-bind="slotProps" @input='debouncedusernamefn'/>
     </template>
const debouncedusernamefn = useDebounceFn(async() => {
   ///mimicking a database true response
   if (form.values.username==='Nana'){
    const { valid, errors } = await form.validateField('username')
    if (valid ){form.setFieldError('username', 'this username is already taken')}
  }
}, 700)

I don't know if I am not doing it well or anything...any help please?

Nyantekyi commented 2 months ago

@razbakov Not exactly... but this is an interesting observation and one id like to avoid. I am about to try something like this now... I will get back to you when it works for me

Nyantekyi commented 2 months ago

@razbakov so I kinda figured it out... however I am still struggling with the hydration issues on the label and description...which honestly should not be my headache ... which I know I can overcome by passing them in as custom props. here's what I came up with

<script setup lang="ts">
import { computed } from "vue";
import type { FieldProps } from "./interface";
import {
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { AutoFormLabel } from "@/components/ui/auto-form";
import { beautifyObjectName, getBaseType } from "./utils";
import { Icon } from "@iconify/vue";
import {
  ComboboxAnchor,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxLabel,
  ComboboxRoot,
  ComboboxSeparator,
  ComboboxTrigger,
  ComboboxViewport,
} from "radix-vue";
const options = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"];
const v = ref("");
const props = defineProps<FieldProps>();
</script>

<template>
  <FormField v-slot="slotProps" :name="fieldName">
    <FormItem v-bind="$attrs">
      <AutoFormLabel v-if="!config?.hideLabel" :required="required">
        {{ config?.label || beautifyObjectName(label ?? fieldName) }}
      </AutoFormLabel>
      <FormControl>
        <!-- <CustomInput v-bind="slotProps" /> -->
        <!-- <Input v-bind="slotProps" >

        </Input> -->
        <slot v-bind="slotProps">
          <ComboboxRoot
            class="relative w-full"
            v-if="config?.component === 'search'"
            :disabled="disabled"
            v-bind="{ ...slotProps.componentField }"
          >
            <ComboboxAnchor
              class="flex justify-between h-10 w-full items-center rounded-md border border-input bg-background px-3 py-2 ring-offset-background focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
            >
              <ComboboxInput
                class="!bg-transparent outline-none text-grass11 h-full selection:bg-grass5 placeholder-mauve8"
                :placeholder="config?.inputProps?.placeholder"
                v-model="v"
              />
              <ComboboxTrigger
                class="inline-flex shrink-0 cursor-pointer items-center justify-center"
              >
                <Icon icon="radix-icons:chevron-down" class="h-4 w-4" />
              </ComboboxTrigger>
            </ComboboxAnchor>
            <ComboboxContent
              class="absolute bg-white overflow-hidden overflow-y-auto border-gray-200 dark:bg-neutral-900 dark:border-neutral-700 z-50 max-h-[300px] w-[var(--radix-popper-anchor-width)] min-w-full rounded-md border bg-popover p-1 text-accent-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
            >
              <ComboboxViewport>
                <ComboboxEmpty />

                <ComboboxGroup>
                  <ComboboxLabel
                    class="px-2 py-1.5 pl-9 text-sm font-medium text-muted-foreground"
                  >
                    Fruits
                  </ComboboxLabel>

                  <ComboboxItem
                    v-for="(option, index) in options"
                    :key="index"
                    class="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 pl-9 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50"
                    :value="option"
                  >
                    <span
                      class="absolute inset-y-0 left-2 flex items-center justify-center"
                    >
                      <ComboboxItemIndicator>
                        <Icon icon="radix-icons:check" class="h-4 w-4" />
                      </ComboboxItemIndicator>
                    </span>

                    <span>
                      {{ option }}
                    </span>
                  </ComboboxItem>
                  <ComboboxSeparator class="h-[1px] bg-grass6 m-[5px]" />
                </ComboboxGroup>
              </ComboboxViewport>
            </ComboboxContent>
          </ComboboxRoot>
        </slot>
      </FormControl>
      <FormDescription v-if="config?.description">
        {{ config.description }}
      </FormDescription>
      <FormMessage />
    </FormItem>
  </FormField>
</template>
Nyantekyi commented 2 months ago

Ow and don't forget to add this to the ui component library in auto forms and update the index.ts and constants.ts... so you can call it like this

 <AutoForm
    class="w-2/3 space-y-6"
    :schema="schema"
    :field-config="{
      username: {
        hideLabel: true,
        component: 'search',
      },
    }"
    @submit="onSubmit"
  >

in your form component

DanielBello7 commented 1 month ago

I'm really sorry for coming in between, but why not just go ahead with the conventional method for form creation, but make use of the styles provided by shadcn and use the toast system for error reporting instead, I find it to be extremely easy and efficient while delivering the best and similar functionality as other forms would

Nyantekyi commented 1 month ago

Well for my project I have a lot of forms I need to build all of which I have a zod schemas for…and a lot. of api calls here and there so...

pheakdey-vtech commented 1 month ago

This is great, but I have encounter difficulties in managing 3 or more level of nesting schema.

razbakov commented 1 month ago

@pheakdey-vtech please elaborate and show some examples. What's your suggestions?

pheakdey-vtech commented 1 month ago

@pheakdey-vtech please elaborate and show some examples. What's your suggestions?

As I have schema inside schema, I need to separate many components in results of difficulty in control slots and its layout. Plus the accordion for object schema doesn't seem to fit in most of use cases.

razbakov commented 1 month ago

@pheakdey-vtech how is it related to this issue?

razbakov commented 1 month ago

@Nyantekyi can you please remove your comments as they are not related to this issue

pheakdey-vtech commented 1 month ago

@pheakdey-vtech how is it related to this issue?

Screenshot 2024-08-22 at 5 59 15 in the afternoon

Here is what it's like, and I would love to hear from you for any suggestion regarding nesting schema case.

razbakov commented 1 month ago

@pheakdey-vtech how is it related to what I described in the issue? Is your question about AutoForm, zod or something else? And what's your question?