vantezzen / auto-form

🌟 A React component that automatically creates a @shadcn/ui form based on a zod schema.
https://vantezzen.github.io/auto-form/
MIT License
2.8k stars 100 forks source link

use "fieldConfig" directly in the zod schema #105

Open leomerida15 opened 4 days ago

leomerida15 commented 4 days ago

In my project I made several changes to improve this library and I think this is one that can help a lot to make the code cleaner and it is very simple to do, if you like it I can add it to the library.

  1. Extend zod so that we can use "fieldConfig" with native method and receive the configuration object.
import { z } from "zod";
import { FieldConfigItem } from "../components/ui/auto-form/types";

// Añadir el método fieldConfig al prototipo de ZodType
z.ZodType.prototype.fieldConfig = function (config: FieldConfigItem) {
    // Guardar la configuración en _def.fieldConfig
    this._def.fieldConfig = config;

    // Retornar this para permitir encadenamiento
    return this;
};

declare module "zod" {
    interface ZodType<Output = any, Def = any, Input = Output> {
        fieldConfig(config: FieldConfigItem): this;
    }
}

export { z };
  1. Define the scheme: for the example of confirming password
const formSchema = z
    .object({
        email: z.string().email(),
        password: z.string(),
        confirm: z.string(),
    })
    .refine((data) => data.password === data.confirm, {
        message: "Passwords must match.",
        path: ["confirm"],
    });
const formSchemaExample = z.object({
    email: z.string().email(),
    password: z.string().fieldConfig({
        inputProps: { type: "password", placeholder: "••••••••" },
        label: "Confirm Password",
    }),
    confirm: z.string().superRefine((confirmValue, ctx) => {
        const { password } = ctx.parent; // Accede al campo password desde el contexto
        if (confirmValue !== password) {
            ctx.addIssue({
                path: ["confirm"], // Ruta vacía para asignar el error directamente a 'confirm'
                message: "Passwords must match.",
                code: z.ZodIssueCode.custom,
            });
        }
    }),
});
  1. How do we get these settings in the component?

}

moltco commented 4 days ago

I like this but I would keep the ability to insert fieldConfig separately as well (as it is today) and maybe have a merging utility to help alter fieldConfig/zod at runtime.

The use case I have in mind are situations where you either want a slight variation of form, e.g. CRUD forms where for the Update you want to set current data as defaultValue. Even for Create you may want to have dynamic default value based on the state of the system. You may also want to vary the input labels as well.

For this scenario to be developer-friendly, and for code reuse/maintenability, I believe there should be a utility/helper method to update the 'static' Zod or fieldConfig configuration so one can inject current defaultValue, label, style, etc.

I would also keep the current ability to provide fieldConfig via props as this adds to flexibility to cater for different scenarios.

Finally, another issue is precendence of values used. currently auto-form appears to be preferring Zod over anything else (see https://github.com/vantezzen/auto-form/issues/98). I think the order of preference, especially for default values (but for anyting else as well) should be:

a) zod, then b) fieldConfig (if implemented into base), then c) fieldConfig provided as form params.

leomerida15 commented 3 days ago

@moltco With a dynamic schema, what exactly do you mean?

That in the Zod schema you can do a setValue?

moltco commented 3 days ago

@leomerida15 - something like that. What I dislike about zod is that it is quite hard (too many steps) to get to schema and manipulate it. It seems to be designed for up-front static schemas in mind. So I would prefer a utility method where you can just do setValue(fieldId, attribute, value) or something similar rather than having to parse through entire schema every time, do checks, etc. in any case it is a good idea to try to abstract zod for future interchangeability/flexibility

I found this (haven't read it fully yet) but it looks as setting value when the data is parsed, not before. https://github.com/colinhacks/zod#parseasync

leomerida15 commented 3 days ago

I have managed to give more dynamics to the zod scheme by placing it inside a "useState"

moltco commented 2 days ago

I have done the same / using state. Now glancing at the code I have used AutoForm values prop to pass 'current' values so my recollection of issues around setting values is probably just bad memory - apologies, my head was in a different space. I will be using AutoForm more over the next couple of weeks and it would be great to be able to add fieldConfig to zod schema + fingers crossed zod-prisma will not remove it from prisma's schema😄

vantezzen commented 2 days ago

Looks interesting - but yes this should definitely opt-in and not pollute the zod prototype by default. I think in some use-cases the schemas are automatically generated so they can't be customized like this but we can just merge the fieldConfig prop with this new fieldConfig entry then.

leomerida15 commented 2 days ago

@vantezzen we can make the "z.object" global

const formSchema = z.object({
    last_name: z
            .string()
            .email()

}).fieldConfig({
    last_name: {
            inputProps: {
            type: "text",
            placeholder: "last name (example)",
        },
        label: "last name",
    }
});
vantezzen commented 2 days ago

I experimented with this a bit but - at least for me - changing the zod prototype directly seems hacky, especially as it's not officially supported by zod and creates some TypeScript problems. It might be better to hook into existing extension points like the superRefine function - I'll play with that a bit to see if something looks better.

vantezzen commented 2 days ago

Here's how this could look like: https://github.com/vantezzen/auto-form/tree/feat/superrefine#field-configuration

leomerida15 commented 2 days ago

I really like that implementation but how can the component access the information?

vantezzen commented 2 days ago

For components there is no difference to the old implementation as the embedded fieldConfig extracted and merged to the fieldConfig prop before rendering:

https://github.com/vantezzen/auto-form/blob/e7a6a27e2218223a2bdc684bf043eb3c7f95c6dc/src/components/ui/auto-form/index.tsx#L63

The superRefine function is just an empty function (so we can attach data without changing how the schema functions) with the field config attached as an object key:

https://github.com/vantezzen/auto-form/blob/e7a6a27e2218223a2bdc684bf043eb3c7f95c6dc/src/components/ui/auto-form/utils.ts#L207