auto-form icon indicating copy to clipboard operation
auto-form copied to clipboard

use "fieldConfig" directly in the zod schema

Open leomerida15 opened this issue 1 year ago • 4 comments

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
  • old scheme:
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"],
	});
  • new scheme: We must change the ".refine" to a ".super Refine" is the only major change that has been found.
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?
  • 1 en auto-form/utils add this function, try to keep the old way to avoid breaking old projects.
export const getFieldConfig = <SchemaType extends ZodObjectOrWrapped>(
	schema: SchemaType,
	fieldConfig?: FieldConfig<z.infer<SchemaType>>,
) => {
	const shape = (schema as z.ZodObject<any>).shape;

	const entry = Object.entries<ZodObject<any, any>>(shape)
		.filter(([, value]) => value?._def?.fieldConfig)
		.map(([key, value]) => {
			return [key, value._def.fieldConfig];
		});

	const resp = Object.fromEntries(entry) as Record<
		keyof typeof shape,
		FieldConfigItem
	>;

	return { ...fieldConfig, ...resp };
};
  • 2 in auto-form/index.tsx
export function AutoForm<SchemaType extends ZodObjectOrWrapped>({
	formSchema,
	values: valuesProp,
	onValuesChange: onValuesChangeProp,
	onParsedValuesChange,
	onSubmit: onSubmitProp,
	fieldConfig: fieldConfigProps,
	children,
	className,
	dependencies,
}: AutoFormProps<SchemaType>) {
	const fieldConfig = getFieldConfig<SchemaType>(formSchema, fieldConfigProps);

	const objectFormSchema = getObjectFormSchema(formSchema);
	const defaultValues: DefaultValues<z.infer<typeof objectFormSchema>> | null =
		getDefaultValues(objectFormSchema, fieldConfig);

	 
}

leomerida15 avatar Oct 05 '24 19:10 leomerida15

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.

moltco avatar Oct 06 '24 10:10 moltco

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

That in the Zod schema you can do a setValue?

leomerida15 avatar Oct 06 '24 22:10 leomerida15

@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

moltco avatar Oct 07 '24 06:10 moltco

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

leomerida15 avatar Oct 07 '24 13:10 leomerida15

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😄

moltco avatar Oct 07 '24 18:10 moltco

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.

vantezzen avatar Oct 08 '24 07:10 vantezzen

@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",
	}
});

leomerida15 avatar Oct 08 '24 13:10 leomerida15

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 avatar Oct 08 '24 14:10 vantezzen

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

vantezzen avatar Oct 08 '24 14:10 vantezzen

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

leomerida15 avatar Oct 08 '24 15:10 leomerida15

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

vantezzen avatar Oct 08 '24 15:10 vantezzen