Giter Site home page Giter Site logo

edmundhung / conform Goto Github PK

View Code? Open in Web Editor NEW
1.3K 1.3K 81.0 6.18 MB

A type-safe form validation library utilizing web fundamentals to progressively enhance HTML Forms with full support for server frameworks like Remix and Next.js.

Home Page: https://conform.guide

License: MIT License

JavaScript 0.80% TypeScript 98.62% Shell 0.01% CSS 0.56%
constraint-validation form-validation nextjs progressive-enhancement react react-router remix-run validation

conform's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

conform's Issues

Hydration issue with client-side validation

Hi! I want to get your thoughts on a problem that we are facing while working with client-side validation.

Background
We are using zod with conform, and our primary use case is client side validation on blur and revalidation on input. We are also using custom input component build on top of Radix.

Problems
Whenever the page loads, the zod schema check doesn't happen until the client side code runs (that's my hypothesis) because of which required flags (like *) etc don't show up in the server rendered page and hence hydration fails on rendered version mismatch
Screenshot 2023-06-16 at 12 06 23

I suspect I must be doing something wrong here, or could it be that it is an actual bug.

The following is a simplified version of my route

export const schema = z.object({
  name: z
    .string()
    .nonempty({ message: "name cannot be empty" })
    .min(3, { message: "name must be at least 3 characters long" }),
  gender: z.string().nonempty({ message: "please select a gender" }),
});

export const action = async (args: ActionArgs) => {
  const formData = await args.request.formData();
  const submission = await parse(formData, {
    schema,
    async: true,
  });

  if (!submission || submission.intent !== "submit" || !submission.payload) {
    return json(submission);
  }
  return { json: submission.payload };
};

export default function UserProfile() {
  const [genderRef, genderControl] = useInputEvent();

  const lastSubmission = useActionData<typeof action>();
  
  const [form, { name, gender }] = useForm<z.input<typeof schema>>({
    lastSubmission,
    constraint: getFieldsetConstraint(schema),
    noValidate: false,
    shouldValidate: "onBlur",
    shouldRevalidate: "onInput",
    onValidate({ formData }) {
      return parse(formData, { schema });
    },
  });

  return (
    <Form method="post" {...form.props}>
      <div className="flex">
        <button type="submit">Submit</button>
      </div>
      <div className="grid grid-cols-2 gap-6 rounded-xl border px-6 py-5">
      
        <input
          required
          minLength={3}
          {...conform.input(name, { type: "text" })}
        />
        {name.error && <p>{name.error}</p>}
        
        <>
          <input
            ref={genderRef}
            {...conform.input(gender, { hidden: true })}
            onFocus={() => genderRef.current?.focus()}
          />
          // Custom select component based on Radix
          <Select
            data={["male", "female", "diverse"]}
            required
            errorMessage={gender.error}
            onValueChange={genderControl.change}
            onOpenChange={(open) => {
              if (!open) genderControl.blur();
            }}
          />
        </>
      </div>
    </Form>
  );
}

Setting form state from useLoaderData hook

Hi, I just want to first think you and evert contributor for the amazing work you have done with this library.

I just wanted to know if there is an example for setting the useForm state from the data returned from a loader?

Conform passes in non-recognized props to <input> fields

When using Conform within Remix Epic Stack, I receive warnings for three different props that Conform passes in.

Warning: React does not recognize the `initialError` prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase `initialerror` instead. If you accidentally passed it from a parent component, remove it from the DOM element.

This same error is repeated for descriptionId and errorId.

I believe the solution is to just rename those props to be 100% lowercase, but I'd love to understand if there's a particular reason behind this naming scheme.

How to send custom errors with 0.6.0

Before 0.6.0 I can send errors using

error: ["", "My custom error"],

On 0.6.0 how I can send custom errors to be able to use it on form.error?

Form error won't display if there are additional buttons before the submit

I'm running into an odd bug on my form, I have buttons that aren't the submit button before the submit button in the DOM, and when they are there, the top level form.error will not display (set to an empty string). But if I remove those buttons, or add a hidden submit input before them, it works correctly.

https://stackblitz.com/edit/node-ygrrtp?file=app/routes/form-error-broken.tsx

I'm more than happy to provide more info to help diagnose. This is a pretty stellar library, especially for working with Remix.

CSS :valid selector with zod

Is there a way to get the CSS :valid selector to only be active after the initial submission using zod? For example in this sandbox, the :valid selector is already active before the form has been submitted, but ideally it would only show after it passes client-side validation after initial submission.

imperative commands for useFieldList

Is there a way to trigger commands from useFieldList imperatively instead of getting button props?
I'm trying to have drag and drop reordering and finding it pretty hard to do.

Zod optional, nullable or nullish don't work when having previous validations

I was following along with epic stack and decided to try making the password field on login page optional

export const loginFormSchema = z.object({
  username: usernameSchema,
  password: z
    .string()
    .min(6, {message: 'Password is too short'})
    .max(100, {message: 'Password is too long'})
    .optional(),
  redirectTo: z.string().optional(),
  remember: checkboxSchema(),
})

I'm not sure if this is a problem with the @conform-to/zod only or it extends to the @conform-to/react but it somehow just ignores the optional in favor of the previous rules.

I can easily see some situations where nullish, nullable and optional are important.

As a workaround I had to make it like this

  password: z.string().optional().or(z
    .string()
    .min(6, {message: 'Password is too short'})
    .max(100, {message: 'Password is too long'})),

Inserting ReCaptcha 3 token in FormData before submit

Firstly, thank you for making this awesome form validation library. Evaluated many and finally settled on Conform as it works really well with Remix.

My question pertains to implementing ReCaptcha 3 along with a Form. I need to send a Captcha token with FormData which can be validated server-side in the action. The token must be generated and inserted into the FormData before the request is sent.

How should I go about doing this? Is there an obvious way to do this that I am missing? I read the documentation thoroughly but couldn't find anything that could help me in this.

Thank you!

0.7.0 not working with remix blues-stack

Not sure what is happening, but when I try to run the build script with the new version, I get this error:

โœ˜ [ERROR] Could not resolve "@conform-to/react/helpers"

    app/routes/creator.dashboard.tsx:2:22:
      2 โ”‚ import { input } from "@conform-to/react/helpers";
        โ•ต                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~

  The path "./helpers" is not exported by package "@conform-to/react":

    node_modules/@conform-to/react/package.json:10:12:
      10 โ”‚   "exports": {
         โ•ต              ^

  You can mark the path "@conform-to/react/helpers" as external to exclude it from the bundle, which will remove this error.

any idea?

What's the recommended way to check if the form state is dirty?

Hi @edmundhung thanks for creating Conform, first of all.

I'm looking for a recommended or better way that fits with Conform when handling dirty fields. My final intention is to disable the button (or prevent submit action) when there's no input change.

Should we solve it using plain useState or is any better recommendation? I'm also thinking if the solution will relate to the lastSubmission object.

As a comparison, in React Hook Form v7, we can retrieve the formState and get the isDirty value like this.

const {
  formState: { isDirty, dirtyFields },
  setValue,
} = useForm({ defaultValues: { test: "" } });

// isDirty: true
setValue('test', 'change')
 
// isDirty: false because there getValues() === defaultValues
setValue('test', '')

Reference:

In Formik v2 we can also retrieve the dirty value from the props.

References:

Thank you!

Reset list to empty array?

Is it possible to reset a list to an empty array (the default value for my use case) in an imperative way? I was looking at the list helper but it does not have a function for that and calling requestIntent multiple times in a loop doesn't seem to work.

Field errors and description

Love that conform handle errors for me in an accessible way. But one question, should it use aria-errormessage instead of aria-describedby?

I would also like to have conform handle field descriptions for me. Adding aria-describedby with a unique id if description is set to true. Maybe the api could look something like this:

<input {...conform.input({ ...email, description: true }, { type: "email" })} />
<p id={email.descriptionId}>Company email</p>

How to deal with number inputs?

Hi again!

I wonder how to deal with number inputs? As per type="number" input spec, it has an available property call valueAsNumber. I know that conform does not coerce anything #107 but if it's a native behaviour, why not do it? I saw checkbox and radio specific code in this library so why not number? Is this something I could help you with?

Client-side validation no longer working

Hi,

I had client-side validation working onBlur, but now it no longer validates client-side -- only server-side. Here is my code:

schema.ts

import { z } from 'zod';
import { getUserByEmail, getUserByUsername } from '~/models/user';

async function isEmailUnique(email: string) {
  const emailTaken = await getUserByEmail(email);
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(emailTaken?.email !== email);
    }, 300);
  });
}

async function isUsernameUnique(username: string) {
  const usernameTaken = await getUserByUsername(username);
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(usernameTaken?.username !== username);
    }, 300);
  });
}

export const registerSchema = z
  .object({
    email: z
      .string()
      .min(1, `Email is required`)
      .email(`Please enter a valid email address`),
    username: z.string().min(1, `Username is required`),
    password: z
      .string()
      .min(8, `Your password needs to be at least 8 characters`)
      .regex(
        /(?=.*[a-z])(?=.*?[A-Z]).*/,
        `Your password must have at least one uppercase and one lowercase character`
      ),
    redirectTo: z.optional(z.string()),
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    path: ["confirmPassword"],
    message: `Passwords do not match`,
  })
  .refine(
    async (data) => {
      await isEmailUnique(data.email);
    },
    {
      path: ["email"],
      message: `That email address has already been registered. Please enter a new one or navigate to the login page`,
    }
  )
  .refine(
    (data) => {
      isUsernameUnique(data.username);
    },
    {
      path: ["username"],
      message: `That username has already been taken. Please choose another`,
    }
  );

/routes/register.tsx

import { parse } from '@conform-to/zod';
import { useForm } from '@conform-to/react';
import { z } from 'zod';
import { createUser } from '~/models/user';

{...}

export const action = async ({request}: ActionArgs) => {
  const formData = await request.formData();
  const submission = await parse(formData, {
    schema: registerSchema,
    async: true,
  }); 

  if (!submission.value?.email || submission.intent !== "submit") {
    return json(
      { ...submission, payload: { email: submission.payload.email } },
      { status: 400 }
    );
  }
  if (!submission.value?.username || submission.intent !== "submit") {
    return json(
      { ...submission, payload: { username: submission.payload.username } },
      { status: 400 }
    );
  }

  try {
    const user = await createUser(
      submission.value?.email,
      submission.value?.username,
      submission.value?.password
    );
    return createUserSession({
      request,
      userId: user.id,
      remember: false,
      redirectTo: submission.value?.redirectTo
        ? submission.value?.redirectTo
        : "/bottles",
    });
  } catch (error) {
    throw new Error(`Could not submit form ${JSON.stringify(error)}`);
  }
} 

{...}

export default function RegisterRoute() {
  const [searchParams] = useSearchParams();
  const lastSubmission = useActionData<typeof action>();
  const [form, { email, username, password, confirmPassword }] = useForm<
    z.input<typeof registerSchema>
  >({
      id: "join",
      lastSubmission,
      onValidate({ formData }) {
        return parse(formData, { schema: registerSchema });
      },
      shouldRevalidate: "onBlur",
    });
  }

return (
   <div>
        <label
          htmlFor={email.id}
          className="block text-sm font-medium text-gray-700"
        >
          Email address
        </label>
        <div className="mt-1">
          <input
            autoComplete="email"
            aria-invalid={email.error ? true : undefined}
            aria-describedby={email.errorId}
            className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
            {...conform.input(email, { type: "email" })}
          />
          {email.error ? (
            <div
              className="mt-1 w-auto rounded bg-red-200 px-2 py-4 text-red-600 shadow-md"
              id={email.errorId}
              role="alert"
            >
              {email.error}
            </div>
          ) : null}
        </div>
      </div>

    { ...other fields}
)

As far as I can tell from the docs (https://conform.guide/validation#client-validation) the "hook" to enable client-side validation is to add the onValidate function (along with the optional shouldRevalidate). However, even with the onValidate function being defined in the useForm hook, client-side validation is still not working. It's not working whether I add a shouldRevalidate option or not.

Let me know if I can provide any other information. Thanks!

Support multiple values for a field ?

I couldn't find any example of handling multiple values for a field. A common example would be a custom element with multiple select options. what's the recommended way to handle this case?

Manage IDs for me (among other things)

With fields, you have to have unique IDs to associate inputs and labels as well as errors (the aria-errormessage prop on the input should be assigned to the ID of the error message element if there is an error, and it should be undefined if not).

I think conform could definitely manage all this for me. I tried migrating one of my existing forms to conform and I ran into a few things that I think conform should do itself. I added โ— next to comments that I want to call out specifically:

import type { DataFunctionArgs } from '@remix-run/node'
import { json } from '@remix-run/node'
import { Link, useFetcher, useNavigate } from '@remix-run/react'
import { useEffect, useRef, useState } from 'react'
import { AuthorizationError } from 'remix-auth'
import { FormStrategy } from 'remix-auth-form'
import { useForm, parse, conform } from '@conform-to/react'
import { formatError, getFieldsetConstraint } from '@conform-to/zod'
import { z } from 'zod'
import { authenticator } from '~/utils/auth.server'
import { commitSession, getSession } from '~/utils/session.server'
import { passwordSchema, usernameSchema } from '~/utils/user-validation'
import { ErrorList, useFocusInvalid } from '~/utils/forms'

export const LoginFormSchema = z.object({
	username: usernameSchema,
	password: passwordSchema,
	remember: z.boolean().optional(),
})

export async function action({ request }: DataFunctionArgs) {
	const formData = await request.clone().formData()
	const submission = parse(formData)
	const result = LoginFormSchema.safeParse(submission.value)
	if (!result.success) {
		// โ— it's uncertain to me whether this handles formErrors and fieldErrors
		// or if it's all just flattened...
		submission.error.push(...formatError(result.error))
		return json({ status: 'invalid-form', submission } as const)
	}
	if (submission.type !== 'submit') {
		return json({ status: 'valid-form', submission } as const)
	}

	let userId: string | null = null
	try {
		userId = await authenticator.authenticate(FormStrategy.name, request, {
			throwOnError: true,
		})
	} catch (error) {
		if (error instanceof AuthorizationError) {
			return json(
				{
					status: 'auth-error',
					errors: {
						formErrors: [error.message],
						fieldErrors: {},
					},
				} as const,
				{ status: 400 },
			)
		}
		throw error
	}

	const session = await getSession(request.headers.get('cookie'))
	session.set(authenticator.sessionKey, userId)
	const { remember } = result.data
	const newCookie = await commitSession(session, {
		maxAge: remember
			? 60 * 60 * 24 * 7 // 7 days
			: undefined,
	})
	return json({ status: 'success', errors: null } as const, {
		headers: { 'Set-Cookie': newCookie },
	})
}

export function InlineLogin({ redirectTo }: { redirectTo?: string }) {
	const loginFetcher = useFetcher<typeof action>()
	const navigate = useNavigate()
	const formRef = useRef<HTMLFormElement>(null)
	const [form, { username, password, remember }] = useForm({
		constraint: getFieldsetConstraint(LoginFormSchema),
		state:
			loginFetcher.data?.status === 'invalid-form'
				? loginFetcher.data.submission
				: undefined,
	})

	// โ— I don't want to have to set these at all. It should be possible to
	// override, but these should just default for me.
	const usernameId = 'username'
	const usernameErrorId = 'username-error'
	const passwordId = 'password'
	const passwordErrorId = 'password-error'
	const rememberId = 'remember'
	const fields = {
		username: {
			labelProps: { htmlFor: usernameId },
			fieldProps: {
				id: usernameId,
				'aria-errormessage': username.error ? usernameErrorId : undefined,
				...conform.input(username.config),
			},
			// โ— I'd really prefer it to be possible to have an array of errors
			errorUI: username.error ? (
				<ErrorList errors={[username.error]} id={usernameErrorId} />
			) : null,
		},
		password: {
			labelProps: { htmlFor: passwordId },
			fieldProps: {
				id: passwordId,
				'aria-errormessage': password.error ? passwordErrorId : undefined,
				...conform.input(password.config),
			},
			errorUI: password.error ? (
				<ErrorList errors={[password.error]} id={passwordErrorId} />
			) : null,
		},
		remember: {
			labelProps: { htmlFor: rememberId },
			fieldProps: {
				id: rememberId,
				...conform.input(remember.config),
			},
			errorUI: null,
		},
	} as const

	const formErrorUI = form.error ? (
		<ErrorList errors={[form.error]} id="form-error" />
	) : null

	const success = loginFetcher.data?.status === 'success'
	useEffect(() => {
		if (!redirectTo) return
		if (!success) return

		navigate(redirectTo)
	}, [success, redirectTo])

	// โ— doing this to be able to control the noValidate prop
	const [hydrated, setHydrated] = useState(false)
	useEffect(() => {
		setHydrated(true)
	}, [])

	// โ— this would be really nice to have built-in
	useFocusInvalid(formRef.current, {
		formErrors: [form.error],
		fieldErrors: {
			username: [username.error],
			password: [password.error],
		},
	})

	return (
		<div>
			<div className="mx-auto w-full max-w-md px-8">
				<loginFetcher.Form
					method="post"
					action="/resources/login"
					className="space-y-6"
					// โ— I have to manually set the aria-errormessage here
					aria-errormessage={formErrorUI ? 'form-error' : undefined}
					{...form.props}
					// โ— I have to override the ref ๐Ÿ˜ฌ I should be able to provide my own
					// ref without messing up conform...
					ref={formRef}
					// โ— I'm passing noValidate here to make sure noValidate only gets set
					// to true once we've successfully hydrated
					noValidate={hydrated}
				>
					<div>
						<label
							className="block text-sm font-medium text-gray-700"
							{...fields.username.labelProps}
						>
							Username
						</label>
						<div className="mt-1">
							<input
								autoComplete="username"
								className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
								{...fields.username.fieldProps}
							/>
							{/*
								โ— notice the lack of logic here. I just render it and the logic
								above ensures that the errorUI is `null` if there is no error
							*/}
							{fields.username.errorUI}
						</div>
					</div>

					<div>
						<label
							className="block text-sm font-medium text-gray-700"
							{...fields.password.labelProps}
						>
							Password
						</label>
						<div className="mt-1">
							<input
								autoComplete="current-password"
								className="w-full rounded border border-gray-500 px-2 py-1 text-lg"
								{...fields.password.fieldProps}
								type="password"
							/>
							{fields.password.errorUI}
						</div>
					</div>

					<div className="flex items-center">
						<input
							className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
							{...fields.remember.fieldProps}
							// โ— I have to manually set the type here. I'd love to either have a
							// conform.checkbox or have conform.input be able to handle this automatically
							type="checkbox"
						/>
						<label
							className="ml-2 block text-sm text-gray-900"
							{...fields.remember.labelProps}
						>
							Remember me
						</label>
					</div>

					{formErrorUI}

					<div className="flex items-center justify-between gap-6">
						<button
							type="submit"
							className="w-full rounded bg-blue-500  py-2 px-4 text-white hover:bg-blue-600 focus:bg-blue-400"
						>
							Log in
						</button>
					</div>
				</loginFetcher.Form>
				<div className="flex justify-around pt-6">
					<Link to="/signup" className="text-blue-600 underline">
						New here?
					</Link>
					<Link to="/forgot-password" className="text-blue-600 underline">
						Forgot password?
					</Link>
				</div>
			</div>
		</div>
	)
}

There are several things in this code example that I think could use some cleanup and help from the conform library. My fields object I think has things that conform could do for me.

It's possible conform does handle some or all of this stuff and I just missed it in the docs. Any direction is welcome.

Reset doesn't work after first submission using new method

Hey,

Really enjoying using conform. Just trying out the new reset method in v0.7.0 and can't get it to work, after the second submission it won't reset.

Here is a minimal example: https://codesandbox.io/p/sandbox/jovial-field-h3qckx?embed=1&file=%2Fapp%2Froot.tsx%3A43%2C18

If you submit a value in the text box it will be cleared the first time, afterwards it won't get cleared if you try another input.

Pretty sure it's something to do with passing in the last submission, and probably a useEffect or something under the hood not being updated with new deps.

conform.input hidden issue

Hi Ed,

When I was using:

<input
{...conform.input(suburb, { hidden: true })}
className="border"
/>

Hidden props not working.
And there is a warning:

React does not recognize the hiddenStyle prop on a DOM element. If you intentionally want it to appear in the DOM as a custom attribute, spell it as lowercase hiddenstyle instead. If you accidentally passed it from a parent component, remove it from the DOM element.

Custom components that create shadow `<input>` not updating client side

I've got a few custom components that I'm using with Conform. They all update their error messages when submitting, but fixing errors after first submit doesn't make all errors go away.

My TextField works fine:

export let Input = forwardRef<HTMLInputElement, InputProps>(
  (props: InputProps, forwardedRef) => {
    let ref = useRef<HTMLInputElement | null>(null);
    let { labelProps, inputProps, descriptionProps, errorMessageProps } =
      useTextField(props, (forwardedRef as any) || ref);
    let { label, height, className, children } = props;

    return (
      <Stack as="label" space="3" className="text-dark">
        <label className="text-xs font-bold" {...labelProps}>
          {label}
        </label>
        <div className="relative">
          <input
            ref={ref}
            className={classNames(
              className,
              heights[height || "sm"],
              baseClasses(!!props.placeholder),
              props.errorMessage ? "!highlight-negative" : ""
            )}
            {...inputProps}
          />
          {children}
        </div>
        {(props.errorMessage || props.padError) && (
          <div
            {...errorMessageProps}
            aria-hidden={!props.errorMessage ? "true" : undefined}
            className={classNames(
              props.errorMessage ? "opacity-100" : "opacity-0",
              "text-xs text-negative transition-opacity"
            )}
          >
            {props.errorMessage || (
              <span className="invisible">"Error message spacer"</span>
            )}
          </div>
        )}
        {props.description && (
          <div {...descriptionProps} className="text-xs">
            {props.description}
          </div>
        )}
      </Stack>
    );
  }
);

Input.displayName = "Input";

let TextField = (
  { type = "text", className, ...props }: InputProps,
  ref: ForwardedRef<HTMLInputElement>
) => (
  <div className={className}>
    <Input ref={ref} type={type} className={classNames("px-3")} {...props} />
  </div>
);

let _TextField = forwardRef(TextField);
export { _TextField as TextField };

My Select does not:

import type { RefCallback } from "react";
import { useRef, useCallback, useState } from "react";
import type { AriaSelectProps } from "@react-types/select";
import { useSelectState } from "react-stately";
import { AnimatePresence } from "framer-motion";
import {
  useSelect,
  HiddenSelect,
  useButton,
  useOverlayPosition,
} from "react-aria";

import { Stack } from "~/components/layouts";

import { ListBox } from "./ListBox";
import { Popover } from "./Popover";

import { classNames } from "~/helpers/class-names";

export { Item } from "react-stately";

export function Select<T extends object>(
  props: AriaSelectProps<T> & {
    padError?: boolean;
    className: string;
    height?: "sm" | "lg";
  }
) {
  let state = useSelectState(props);

  let ref = useRef<HTMLElement | null>(null);

  let overlayRef = useRef();

  let [rect, setRect] = useState<{ width: number }>();

  let callbackRef: RefCallback<HTMLElement> = useCallback((node) => {
    if (node !== null) {
      ref.current = node;
      setRect(node.getBoundingClientRect());
    }
  }, []);

  let { labelProps, errorMessageProps, triggerProps, valueProps, menuProps } =
    useSelect(props, state, ref);

  let { overlayProps } = useOverlayPosition({
    offset: 4,
    targetRef: ref as any,
    overlayRef: overlayRef as any,
    isOpen: state.isOpen,
    shouldUpdatePosition: true,
    shouldFlip: true,
  });

  let { buttonProps } = useButton(triggerProps, ref);

  let baseClasses = [
    "flex",
    "items-center",
    "w-full",
    "border",
    "rounded",
    "transition",
    "placeholder-light",
    "focus:border-transparent",
    "focus:outline-none",
    "focus:highlight",
  ];

  let heights = {
    button: {
      sm: "h-[38px]",
      lg: "h-10",
    },
    option: {
      sm: "h-[38px]",
      lg: "h-12",
    },
  };

  return (
    <div
      className={classNames(
        "relative inline-flex flex-col gap-3",
        props.className
      )}
    >
      <label
        {...labelProps}
        className="text-gray-700 block cursor-default text-left text-xs font-bold"
        htmlFor={buttonProps.id}
      >
        {props.label}
      </label>
      <HiddenSelect
        state={state}
        triggerRef={ref}
        label={props.label}
        name={props.name}
      />
      <Stack space="3">
        <button
          {...buttonProps}
          ref={callbackRef}
          className={classNames(
            ...baseClasses,
            heights.button[props.height || "sm"],
            props.isDisabled && "opacity-50",
            (state.isOpen || state.isFocused) && "!highlight",
            !state.selectedItem && "highlight-muted",
            state.selectedItem && !state.isOpen
              ? "border-light"
              : "border-transparent",
            "relative cursor-default rounded bg-inverted pl-[10px] pr-10 text-left text-xs"
          )}
        >
          <span
            {...valueProps}
            className={classNames(
              "block truncate",
              !state.selectedItem && "text-light"
            )}
          >
            {state.selectedItem
              ? state.selectedItem.rendered
              : props.placeholder}
          </span>
          <span
            aria-hidden
            className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
          >
            <svg
              width="25"
              height="24"
              viewBox="0 0 25 24"
              fill="none"
              xmlns="http://www.w3.org/2000/svg"
            >
              <path
                d="M17.5795 8.58997L12.9895 13.17L8.3995 8.58997L6.9895 9.99997L12.9895 16L18.9895 9.99997L17.5795 8.58997Z"
                fill="#6E6E6E"
              />
            </svg>
          </span>
        </button>
        {(props.errorMessage || props.padError) && (
          <div
            {...errorMessageProps}
            className={classNames(
              props.errorMessage ? "opacity-100" : "opacity-0",
              "text-xs text-negative transition-opacity"
            )}
          >
            {props.errorMessage || (
              <span className="invisible">"Error message spacer"</span>
            )}
          </div>
        )}
      </Stack>
      <AnimatePresence>
        {state.isOpen && (
          <Popover
            minWidth={rect?.width}
            ref={overlayRef as any}
            {...overlayProps}
            isOpen={state.isOpen}
            onClose={state.close}
          >
            <ListBox {...menuProps} state={state} />
          </Popover>
        )}
      </AnimatePresence>
    </div>
  );
}

Can you see any reason why the Select wouldn't update the errorMessage when selecting an option on a previously errored input?

They both render actual input tags with name attributes set, so useControlledInput doesn't seem necessary.

Is there a way to reset form to default values if they're changed?

For example I'm setting form like this:

const [form, fields] = useForm<PostUpdate>({
    defaultValue: defaultValue,
    lastSubmission: lastSubmission,
    initialReport: "onBlur"
  });

Both defaultValue & lastSubmission are component props that are passed from another component.
When defaultValue changes - nothing changes in form, I've tried setting input 2 ways:

  1. <input type="text" {...conform.input(field)} />
  2. <input type="text" name={field.name} defaultValue={field.defaultValue} />

It looks like form is not being reset if defaultValue changes. Maybe there are any ways to reset it manually to default value?

I've also tried to add useEffect hook:

  const defaultValueDeferred = React.useDeferredValue(defaultValue);
  React.useEffect(() => {
    form.ref.current?.reset();
  }, [defaultValueDeferred, form.ref]);

But it resets to a previous state of the form instead of new one.

And I'm using Remix, so defaultValues are retrieved val loader & lastSubmission via action (maybe problem relates to Remix integration).

Disabling submit button

If I disable the submit button on submission it looks like the state of useForm is "lagging" behind. Clicking the submit button a second time will make it update (and you'll see the error in the example below).

My implementation is in Remix, not sure if that's related. Here's a reproduced example on CodeSandbox

Remix example for signup is not working

Hi!

I noticed that the remix example for signup is not working. When I enter everything correctly, it won't sign up the form.

It might have to do something with the useForm hook. Even if everything is correct, it still falls into the if condition.
image

Double POST with validate/__intent__ ?

Using Remix, I'm finding a weird behaviour for a simple newsletter form.

When I submit, I get this:

image

The first call behaves as expected
image

It does a redirect('/');

The next invocation however looks like this:

image

This obviously fails on the server that's expecting a different intent, thus renders the form invalid - returning the data, filling out the form.

What am I doing wrong?
Where does the validate/intent come from?

My form config:

const [form, { email }] = useForm({
    constraint: getFieldsetConstraint(schema),
    fallbackNative: true,
    lastSubmission: lastSubmission,
    shouldValidate: 'onBlur',
    onValidate: function ({ formData }) {
      const result = parse(formData, { schema: schema });
      setIsValid(result.error === undefined);
      return result;
    }
  });

Some of the other bits:

const NEWSLETTER_INTENT = 'newsletter.signup';

export const schema = z.object({
  email: z.string().min(1, 'validation.email.required').email('validation.email.invalid')
});

export const newsletterSignUpAction: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const submission = parse(formData, { schema: schema });

  console.log({submission: submission});

  if (!submission.value || submission.intent !== NEWSLETTER_INTENT) {
    return json(submission, { status: 400 });
  }

  throw redirect('/');
  // do something with result.data
};

"The intent could only be set on a button" on requestIntent validation

I'm using requestIntent to validate a field when the value changes (requestIntent(form.ref.current, validate(name))) (a legacy component that don't play nice with normal form/form submissions). This works fine in all browsers on latest version. But in Safari 15.6 I'm getting the error "The intent could only be set on a button". Getting the same error client and server side, so I'm wondering if it has something to do with Safari 15.6's implementation of FormData(?)

subscribe to field value ?

I need to subscribe to field values when they change currently the only way is to manage my own state and listen to onChange event which works but it becomes ugly really quick when I need to subscribe to multiple fields. Is there any easier way to achieve this I am looking for an API similar to https://www.react-hook-form.com/api/usewatch/

Doesn't return errors when used with useFetcher

I just tried using conform in a remix project using useFetcher instead of a plain Form and it doesn't seem to be working correctly. I have also been unable to find a guide that explains whether some special steps have to be taken for the useFetcher case.

When I submit the form using Form with wrong input data, I get an error message returned. When I do the same using fetcher.Form I don't. I expect the fetcher.Form behavior to be the same as the Form behavior.

Below is a minimal example using both Form, which works as expected, and useFetcher, where errors aren't returned on submission and therefore not shown.

The working version using `Form`

import { useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { Form, useActionData } from "@remix-run/react";
import type { ActionArgs } from "@remix-run/server-runtime";
import { redirect } from "@remix-run/server-runtime";
import z from "zod";

const schema = z.object({
  name: z.string().nonempty(),
});

export const action = async (args: ActionArgs) => {
  const formData = await args.request.formData();
  const submission = parse(formData, { schema });
  if (!submission.value || submission.intent !== "submit") {
    return submission;
  }
  throw redirect("/");
};

export default function Component() {
  const lastSubmission = useActionData<typeof action>();
  const [form, { name }] = useForm({ lastSubmission });
  return (
    <Form
      method="POST"
      {...form.props}
      className="max-w-screen-sm my-4 mx-auto flex flex-col gap-4"
    >
      <input {...name} className="bg-slate-50 text-slate-950" />
      <span className="text-red-700">{name.error}</span>
      <button className="bg-teal-400 text-teal-950" type="submit">
        Submit
      </button>
    </Form>
  );
}

The not-working version using `useFetcher`

import { useForm } from "@conform-to/react";
import { parse } from "@conform-to/zod";
import { useActionData, useFetcher } from "@remix-run/react";
import type { ActionArgs } from "@remix-run/server-runtime";
import { redirect } from "@remix-run/server-runtime";
import z from "zod";

const schema = z.object({
  name: z.string().nonempty(),
});

export const action = async (args: ActionArgs) => {
  const formData = await args.request.formData();
  const submission = parse(formData, { schema });
  if (!submission.value || submission.intent !== "submit") {
    return submission;
  }
  throw redirect("/");
};

export default function Component() {
  const lastSubmission = useActionData<typeof action>();
  const fetcher = useFetcher();
  const [form, { name }] = useForm({ lastSubmission });
  return (
    <fetcher.Form
      method="POST"
      {...form.props}
      className="max-w-screen-sm my-4 mx-auto flex flex-col gap-4"
    >
      <input {...name} className="bg-slate-50 text-slate-950" />
      <span className="text-red-700">{name.error}</span>
      <button className="bg-teal-400 text-teal-950" type="submit">
        Submit
      </button>
    </fetcher.Form>
  );
}

Output of a diff between the two

3c3
< import { Form, useActionData } from "@remix-run/react";
---
> import { useActionData, useFetcher } from "@remix-run/react";
22a23
>   const fetcher = useFetcher();
25c26
<     <Form
---
>     <fetcher.Form
35c36
<     </Form>
---
>     </fetcher.Form>

Show error validating object from zod client-side

Currently I have an object that is being validated to know if is empty or not (I'm using a combo box) the error is coming from server-side perfectly but if I use client-side I can see the error on onValidate but is not getting the value inside author.error on client-side for some reason ๐Ÿค”

CleanShot 2022-11-26 at 18 43 16

How to use with input type radio elements?

When I use the library like this:

<label>
      <div>Answer<div>
      <input value="a" {...conform.input(answer.config, {type: 'radio'})}/> A
      <input value="b" {...conform.input(answer.config,{type: 'radio'})}/> B
      <div className="field-error">{answer.error}</div>
 </label>

I get the error message Entry with the same name is not supported.

How am I supposed to use validation for this case?

How to perform async client side action after validation and before submit?

I need to integrate Recaptcha check to my forms. I thought best way would be to perform that after validation and it needs to be done before submission. Steps are like this:

a) validate form using conform
b) perform async recaptcha call and get token
c) integrate token value to form data to be submitted together with other validated value

I'm struggling how to perform steps b+c ideally.

Upgrading to 0.6.2 changes validation behavior

I'll try to debug and write this up more fully in the morning, but after upgrading to 0.6.2, one of my forms passes validation after any change, even when there are invalid fields. Reverting to 0.6.1 restores expected behavior.

Form is being submitted on each keypress after an error occurs

Description
Steps:

  • Have a basic form which has validations
  • Submit the form with invalid values
  • Get an error response
  • Try to re-enter values and observe form is being submitted on each keypress

Version: 0.6.0

Repro: https://codesandbox.io/s/brave-lamarr-6s9zbg?file=/app/routes/login.tsx to better observe the issue, open browser window in a new tab and see logs from onSubmit when you type new value after an error.

Actual
Form is unexpectedly submitted on each keypress after an error

Expected
Form should be submitted only when clicking on a submit button

Custom error doesn't show on first form submit

Hi there, saw your talk at Remix Conf and gave the library a try. Big fan!

I am running into one issue that I can't seem to resolve. I have a route /register where a user can create a new account. In the action function for that route, I am using Conform to parse the form data, handle errors, and create the user. I'd like to be able to perform an additional validation step -- checking if the username is already taken -- and return an error if it is. I saw #118 so I tried the following:

export async function action({ request }: ActionArgs) {
  const formData = await request.formData();
  const submission = parse(formData, {schema})

  const usernameTaken = await findUserByUsername(submission.value.username);

  if(usernameTaken) {
    submission.error.username = "That username has already been taken. Please choose another."
    return json(submission, { status: 400 });
  }

 await createUser(submission.value.username, submission.value.password)

  return redirect("/profile")
}

Here's the JSX for that register route:

{...}
<div>
          <label htmlFor={username.id}>
            Username
          </label>
          <input
            aria-invalid={username.error ? true : undefined}
            aria-describedby={username.errorId}
            {...conform.input(username, { type: "text" })}
          />
          <div
            id={username.errorId}
            role="alert"
          >
            {username.error}
            {form.error?.username ? form.error.username : null}
          </div>
        </div>
{...}
<button type="submit">Register</button>

When I submit the form with a known existing username it autofocuses on the username input, suggesting it recognizes an error in the form, but it doesn't show the error message. When I submit the form again it renders the form.error.username error correctly. So it isn't showing me the error on the first submission, but it does on the second, third, etc.

Forward slash in default value with list doesn't work

When I use a string that contains a forward slash as a default value with a list method like append or replace. It triggers the validations for the whole form. If there are no validation errors it doesn't work.

 <button
        {...list.append(tasks.name, {
          defaultValue: { content: "Random /  More Random" },
        })}
      >
        Add task
</button>

Clear the errors

Is there a way to clear the errors using any utility? I'm currently having a product request to make a functionality like this but couldn't figure it out yet

Can't useFieldset on zod optional object

When creating a zod schema containing an optional object, I am unable to use useFieldset on it because the field config for it is possibly undefined. I would expect the field config to always be there, and only have the validation handle the optional part of it.

Example schema:

const schema = z
  .object({
    email: z
      .string()
      .min(1, "Email is required")
      .email("Please enter a valid email"),
    address: z
      .object({
        street1: z.string().min(1, "Street 1 is required"),
        street2: z.string().min(1).optional(),
        city: z.string().min(1, "City is required"),
        state: z.string().min(1, "State is required"),
        postalCode: z.string().min(1).max(5),
      })
      .optional(),
  })

address would come back as type FieldConfig<{street1: string, street2?: string, ...}> | undefined, meaning I couldn't pass it to useFieldset.

Example code sandbox: https://codesandbox.io/p/sandbox/peaceful-mclean-g1mygf?file=%2Fapp%2Froutes%2Findex.tsx%3A8%2C1-28%2C5

See routes/index.tsx:67

Validation triggers when rendering an input conditionally

CleanShot.2022-12-02.at.16.17.56.mp4

I've got some inputs that only render after the user clicks a button to start editing. Whenever the input renders, Conform fires the validation, adds the data-conform-touched attribute, and triggers the error message.

If I don't conditionally render the input, none of that happens.

Is this just me doing something stupid or does it sound like a bug?

v1.0 Checklist

Checklist

  • Conform Playground (#3)
  • Improved controlled component support (e.g. material-ui select) (#19)
  • Better test coverage
  • Simplify example setup (#23)
  • Documentation website (#29)
  • Testing guide (a.k.a. How to test)
  • Button value validation (#21)
  • Better Boolean support (#5)
  • Yup support (#15)
  • Examples with create-react-app
  • Example with next.js
  • Setting up changeset (#2)
  • Autofoucs on error (JS+No-JS) (#33)
  • File upload support (#72)
  • Improve array support (#71)
  • Allows combining list commands
  • Async validation (#40)

Button type button triggers validation when `shouldValidate` is set to `onBlur`

When a <button type="button" /> is included inside of the form with the option of shouldValidate: 'onBlur', whenever that button is clicked, validation is run. My expectation is that this would only be the case for <button type="submit" />. It causes weird behaviors because a button with an action unrelated to validation will trigger validation and I'll see errors pop up all over the screen.

For a replication, I've forked your remix example and added a button at the top of the login form. https://codesandbox.io/p/sandbox/throbbing-bush-f1o7mm
Click the "Click me to go kaboom" button and you'll see validation triggered and error messages everywhere.

"Repeated field name is not supported" when using checkbox

Implementing a form where I have multiple <input type="checkbox" /> that share the same name, but getting the error "Repeated field name is not supported". Can't find anything in the documentation if I need to treat checkboxes differently. Do I need to do that? When not using conform I would get the values with formData.getAll("name") and get an array back of all the checked inputs.

Idea: provide "inProgress" state of form

Would like to know what you think about that idea:

The form could provide some kind of "inProgress" state while validation and submission. That could be used to disable submit button during that phase:

<form method="post" {...form.props}>
    [...]
     <button
        type="submit"
        className={cx('btn', 'btn-item', form.inProgress() && 'loading')}
        disabled={form.inProgress()}
      >Submit</button>
</form>

I could provide a PR for that if you like...

Support a list of errors

zod can return a list of errors for a single field. It would be nice if conform could handle this case and the error for a field could be an array of error messages. An example would be a password field that has multiple requirements.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.