Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Form primitive #1977

Closed
wants to merge 4 commits into from
Closed

RFC: Form primitive #1977

wants to merge 4 commits into from

Conversation

benoitgrelard
Copy link
Contributor

@benoitgrelard benoitgrelard commented Feb 22, 2023

This RFC proposes adding a new primitive to Radix UI primitives: Form. It will provide an easier way to create forms in React. The goal is to provide a simple, declarative, uncontrolled (but controllable) API (à la Radix) to create forms that includes client-side validation in an accessible manner, as well as handling of server errors accessibly too.

View rendered RFC

We would love your input on this 🙏

@peduarte
Copy link

This is great 👏

@Murkrage
Copy link

I think this is great and definitely something I would use. I'm trying to move away from state based forms and into native forms but that does come with its own set of challenges.

I have one question regarding the error type and messages. Perhaps instead of a component for each desired error type you could provide <Form.ClientMessage /> with an object that matches the following: <type, message>. If I wanted to use a wide range of messages, this would eliminate multiple components form having to be created. Many of which will be sharing styling.

Since valid is a type, and usually this would involve some form of green styling, a data attribute could be added to indicate whether the input is valid or invalid: data-state='valid' which can then be used with styling.

@benoitgrelard
Copy link
Contributor Author

benoitgrelard commented Feb 22, 2023

Thanks for the feedback @Murkrage!

I have one question regarding the error type and messages. Perhaps instead of a component for each desired error type you could provide <Form.ClientMessage /> with an object that matches the following: <type, message>. If I wanted to use a wide range of messages, this would eliminate multiple components form having to be created. Many of which will be sharing styling.

That's interesting. Although a potential issue is that it moves us away from the 1:1 mapping between part and elements. As there may be situations where multiple messages are visible at once, you'd generally want access to the element for styling for example.

Since valid is a type, and usually this would involve some form of green styling, a data attribute could be added to indicate whether the input is valid or invalid: data-state='valid' which can then be used with styling.

Yeah that's included in the RFC already in the styling section: https://github.com/radix-ui/primitives/blob/form-rfc/rfcs/2023-radix-form-primitive.md#styling

@Murkrage
Copy link

As there may be situations where multiple messages are visible at once, you'd generally want access to the element for styling for example.

That's not something I had considered, to be honest. I think there's merit for both implementations and ultimately it's subjective whether someone "likes" the extra components for messaging or not. I suppose someone could always create a wrapping component around all of the messages to achieve the same result.

<Form.Field name="name">
<Form.Label>Full name</Form.Label>
<Form.Control />
<Form.ClientMessage type="customError" isValid={(value, fields) => value === 'John'}>
Copy link

@peduarte peduarte Feb 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isValid seems like a boolean prop at first glance, instead of a function. How about validator?

Copy link
Contributor Author

@benoitgrelard benoitgrelard Feb 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point, we can check prior art too. react-hook-forms seem to use validate for custom validation, so perhaps we can go with this too, it definitly is better as a verb I agree.

@ttsirkia
Copy link

If using this, how it would integrate with React Hook Form or similar libraries? Or is the idea that you wouldn't need those anymore?

@bdsqqq
Copy link

bdsqqq commented Feb 22, 2023

Great work on the RFC!

Does this API make sense?

Yes! Combining the browser constraint validation API makes the logic part intuitive, while the Radix pattern of splitting a component into multiple parts + the asChild approach makes composability simple and advanced things like dynamically generated fields frictionless.

As with every other primitive, the data- attributes provide an easy way to style different states, and I especially like the control given by ClientMessage being a component for each error type. For this bit how would someone achieve "I want to add a comma between each of my errors", would checking the validityState after each ClientMessage and conditionally adding a comma be the best/only approach?

The server Errors made sense and having them based on the name of the Form.Field is nice, but I'd having something like <Form.ServerMessage name="email" /> outside a Form.Field would be great.


Also, the isValid prop is already very useful, but are there any plans to integrate with validation libraries that provide a schema like ZOD?

@jamesopstad
Copy link

This is exciting! I'm interested in how this would integrate with the React Router Form component (https://reactrouter.com/en/main/components/form)? Could this be done directly by using asChild on Form.Root or would you have to instead use the useSubmit hook (https://reactrouter.com/en/main/hooks/use-submit) somehow?

@benoitgrelard
Copy link
Contributor Author

If using this, how it would integrate with React Hook Form or similar libraries? Or is the idea that you wouldn't need those anymore?

@ttsirkia Yes that's the idea, it should tackle yours needs around form validation so it wouldn't make much sense to use both. Are there things you can think of that wouldn't be possible with our API but would be with React Hook Form or similar libs?

@benoitgrelard
Copy link
Contributor Author

Thanks for the feedback @bdsqqq!

For this bit how would someone achieve "I want to add a comma between each of my errors", would checking the validityState after each ClientMessage and conditionally adding a comma be the best/only approach?

The use-cases for multiple messages showing at once are limited but could potentially happen so this is a good question. At the moment each ClientMessage would only be aware of its own type and would "show" itself accordingly to that type matches or not. But you are correct that using the ValidityState component would give you access to a more holistic view of the overall validity for that field. That said, I am not sure it would enable you easily to have commas in between. The easiest way to achieve this might actually to wrap all of the messages in an element like a span and do it with css pseudo content, like:

.wrapper .message:not(:last-child):after {
  content: ", ";
}

The server Errors made sense and having them based on the name of the Form.Field is nice, but I'd having something like <Form.ServerMessage name="email" /> outside a Form.Field would be great.

Having a more general access anywhere inside Form by manually providing the name (rather than relying on the context providing by Field) could be on the cards. The reason we are limiting it per field for now is:

  1. This is 99% of the time where you need it
  2. Limits re-rendering only to a single field, rather than re-rendering the whole form each time
  3. The field is just a div so could wrap as high as you can

Also, the isValid prop is already very useful, but are there any plans to integrate with validation libraries that provide a schema like ZOD?

I haven't played much with ZOD, if it gives you a function back to run validation then you should be able to use it in isValid. Are there specific things you can think of here in terms of integration?

@benoitgrelard
Copy link
Contributor Author

This is exciting! I'm interested in how this would integrate with the React Router Form component (https://reactrouter.com/en/main/components/form)? Could this be done directly by using asChild on Form.Root or would you have to instead use the useSubmit hook (https://reactrouter.com/en/main/hooks/use-submit) somehow?

@jamesopstad I am not familiar with React Router Form component, but looking at their docs at a glance, it seems like it could be a match made in heaven.

They specifically mention:

The Form component is a wrapper around a plain HTML form that emulates the browser for client side routing and data mutations. It is not a form validation/state management library like you might be used to in the React ecosystem (for that, we recommend the browser's built in HTML Form Validation and data validation on your backend server).

Which is exactly what ours is.

I think as long as their Form component forwards all props and ref and generally behaves like a normal native form, using asChild should be all one need to compose the 2.

import * as Form from '@radix-ui/react-form';
import { Form as RouterForm } from "react-router-dom";

<Form.Root asChild>
  <RouterForm></RouterForm>
</Form.Root>

One to play around with to check compatibility but I think it's promising.

@jamesopstad
Copy link

@benoitgrelard This sounds very promising!

I think as long as their Form component forwards all props and ref and generally behaves like a normal native form, using asChild should be all one need to compose the 2.

I've just had a quick look at the source code and all of the above are true. The nice thing about the asChild approach is that people can just create their own Form component that composes the two. FYI the the React Router Form component is also used in Remix.

@ttsirkia
Copy link

@ttsirkia Yes that's the idea, it should tackle yours needs around form validation so it wouldn't make much sense to use both. Are there things you can think of that wouldn't be possible with our API but would be with React Hook Form or similar libs?

I could see the benefits of using Radix to provide the accessibility part and React Hook Form the state management and validation (maybe with Zod) etc.

@benoitgrelard
Copy link
Contributor Author

I could see the benefits of using Radix to provide the accessibility part and React Hook Form the state management and validation (maybe with Zod) etc.

@ttsirkia, as far as I understand, React Hook Form doesn't deal with any accessibility (they just have a quick section about it, with not great advice). So I see where you're coming from. That being said it would be quite difficult to dissociate the 2 because they are integrally linked, ie. the accessibility is based off of the native validation too.

@benoitgrelard
Copy link
Contributor Author

I've just had a quick look at the source code and all of the above are true. The nice thing about the asChild approach is that people can just create their own Form component that composes the two. FYI the the React Router Form component is also used in Remix.

@jamesopstad, that's awesome. I'll play around with it but yeah sounds like things should slot in nicely 🙂 And yes that is the nice thing about asChild!

@rothsandro
Copy link

Did you consider supporting schema-based validation with a library like Zod?

@jjenzz
Copy link
Contributor

jjenzz commented Feb 22, 2023

okay, i am so fricken' excited that this is finally happening 🎉 yay!

a few bits i would love to see that i didn't see mentioned (or may have missed, 'pologies). it was common in ye olden' days to be able to name fields like pets[] and PHP would understand that this is an array and give you an array on the server. it would be great if the onSubmit callback was a little smarter (CustomEvent detail or even a separate callback) and gave you the form data object with array values in a similar fashion. for example something like the following:

<Form.Field name="pets[][breed]" >
  <Form.Control type="checkbox" value="dog" />
  <Form.Control type="checkbox" value="cat" />
</Form.Field>

would output:

{ 
  pets: [
    { breed: 'dog' }, 
    { breed: 'cat' }
  ] 
}

here's a version of my own attempts at this declarative Form idea from many moons ago (so ignore how scrappy it is) that also uses named form groups (rendered as fieldsets). these give you nesting in your form data object which might also be a nice addition and further enforce semantics/a11y by encouraging the use of fieldsets for groups of data: https://codesandbox.io/s/form-components-6gdck

another thing perhaps worth pointing out from the above example is that validation is not restricted to being rendered alongside the field itself because it associates itself using a name/field prop relationship much like for/id. so, i can render all my errors at the top of my form/fieldset if that is a preferred design choice. i'm not sure if that is okay in terms of a11y tho, especially if Radix will be moving focus to the first invalid field. i'm assuming the control focus is an a11y requirement? this might make more sense for the server errors rather than the client ones but i like the flexibility it provides.

also, i agree with the isValid concerns. definitely sounds like a boolean. something like validator would be preferred 👍

@jamiebuilds
Copy link

It would be nice to have <Form.Description> for adding aria-describbedby to your field.

@jjenzz
Copy link
Contributor

jjenzz commented Feb 22, 2023

It would be nice to have <Form.Description> for adding aria-describbedby to your field.

@jamiebuilds i presume Radix will do this internally already for you because that is needed to associate Form.ClientMessage with the input for a11y purposes i believe. that will all be part of the a11y wiring work that Radix handles for you.

@jamiebuilds
Copy link

<Form.Field> should optionally accept an id prop so you could pass in your own in cases where you want to use the id outside of its children (ex. <output>, various aria attributes, etc.)

@bdsqqq
Copy link

bdsqqq commented Feb 22, 2023

@benoitgrelard

The easiest way to achieve this might actually to wrap all of the messages in an element like a span and do it with css pseudo content...

This makes a lot of sense. I'll raise this as another pro to exposing each message as a component instead of a single object or something similar.

I haven't played much with ZOD, if it gives you a function back to run validation then you should be able to use it in isValid. Are there specific things you can think of here in terms of integration?

The biggest reasons why someone would use ZOD are to share a validation schema between the back and front end and to be able to infer the type of the form data. I think you could validate each field individually using zod, but the big win would come if you were able to pass the whole schema to the form as a validator.

as an example, react-hook-form would do:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';

const schema = z.object({
  name: z.string().min(1, { message: 'Required' }),
  age: z.number().min(10),
});

const App = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    resolver: zodResolver(schema),
  });

return (
    <form onSubmit={handleSubmit((d) => console.log(d))}>
      <input {...register('name')} />
      {errors.name?.message && <p>{errors.name?.message}</p>}
      <input type="number" {...register('age', { valueAsNumber: true })} />
      {errors.age?.message && <p>{errors.age?.message}</p>}
      <input type="submit" />
    </form>
  );
};

I see why this might not belong in the primitive itself, but I think it would be useful to expose a way for the consumer(or more realistically, library authors) to define custom resolvers that apply to the whole form instead of single fields.


Additionally, this is probably outside of 99% of use cases, but would come up if custom resolvers were a thing.
Is it possible to tap into the default ValidityState values with custom validation?

eg:

<Form.Field name="something">
  <Form.Control type="number"/>
  <Form.ClientMessage type="rangeUnderflow" isValid={(value, fields) => value <= 10}>
    Too big.
  </Form.ClientMessage>
</Form.Field>

instead of

<Form.Field name="something">
  <Form.Control type="number" max="10"/>
  <Form.ClientMessage type="rangeUnderflow">
    Too big.
  </Form.ClientMessage>
</Form.Field>

Again, amazing work on the RFC, excited to see it develop!

rfcs/2023-radix-form-primitive.md Show resolved Hide resolved
rfcs/2023-radix-form-primitive.md Outdated Show resolved Hide resolved
rfcs/2023-radix-form-primitive.md Outdated Show resolved Hide resolved
@benoitgrelard
Copy link
Contributor Author

benoitgrelard commented Mar 7, 2023

Here's the preview link for the upcoming docs of Form: https://radix-website-git-form.modulz-deploys.com/docs/primitives/components/form

Here's the production link now: https://www.radix-ui.com/docs/primitives/components/form

@franciscohanna92
Copy link

franciscohanna92 commented Mar 8, 2023

Really excited about this! 🤯

My two cents:
Field Arrays, available in both react-hook-form#useFieldArray and remix-validated-form#FIeldArray, is one of those features you don't know how much you need until you do. It would be great to have it as a Form primitive.

Not having it would mean some people coming up with their own implementation, whic adds a lot of maintenance to users and would sometimes be a reason to use another lib that supports it (personal experience)

@benoitgrelard
Copy link
Contributor Author

benoitgrelard commented Mar 8, 2023

Really excited about this! 🤯

My two cents: Field Arrays, available in both react-hook-form#useFieldArray and remix-validated-form#FIeldArray, is one of those features you don't know how much you need until you do. It would be great to have it as a Form primitive.

Not having it would mean some people coming up with their own implementation, whic adds a lot of maintenance to users, a would sometimes be a reason to use another lib that supports it (personal experience)

Hey @franciscohanna92 yeah that is something we have been thinking about. We are likely to include an API to handle such cases. That's also one reason for pushing this out in preview, so people can use it in all these kinds of scenarios and so we can see what emerges from it as needs.

@joaom00
Copy link
Contributor

joaom00 commented Mar 14, 2023

Great work on this guys! Really liked it!

Have you thought about supporting multiple validation strategies? I think that currently it is only possible to validate when submit? it would be nice to have something like:

<Form.Field validationStrategy="onSubmit | onBlur | onChange"
  ...
</Form.Field>

as the Constraint validation API is used this can be done on the user-land with reportValidity() but it would be nice if it was builtin

@benoitgrelard
Copy link
Contributor Author

Great work on this guys! Really liked it!

Have you thought about supporting multiple validation strategies? I think that currently it is only possible to validate when submit? it would be nice to have something like:

<Form.Field validationStrategy="onSubmit | onBlur | onChange"
  ...
</Form.Field>

as the Constraint validation API is used this can be done on the user-land with reportValidity() but it would be nice if it was builtin

Yes definitely something on the list, or at least something we thought people would ask for having more control over. For now we went with what we believe is a good default UX-wise but I think there's definitely room for that in the future.

@rostero1
Copy link

Does anyone have an example of how this would work with react-select? An example would be (1) requires a selection and (2) requires a number greater than 5.

@kentcdodds
Copy link

This appears to already be implemented: https://www.radix-ui.com/docs/primitives/components/form

Is there a reason this hasn't been merged?

@benoitgrelard
Copy link
Contributor Author

We kept it open for now as the main place for feedback since the component is in "preview" for now.

@slavanga
Copy link

slavanga commented Apr 21, 2023

I really like how simple and composable the Form primitive is.

One thing that does not work yet is checking if a form is valid without triggering all validation messages. Natively there are checkValidity and reportValidity. Calling checkValidity on the form should just return the status without showing any messages. Are there other ways to achieve this or could this be improved?

@benoitgrelard
Copy link
Contributor Author

I really like how simple and composable the Form primitive is.

One thing that does not work yet is checking if a form is valid without triggering all validation messages. Natively there are checkValidity and reportValidity. Calling checkValidity on the form should just return the status without showing any messages. Are there other ways to achieve this or could this be improved?

We are building on top of the native functionality, so you should still be able to call checkValidity.

@slavanga
Copy link

slavanga commented Apr 24, 2023

@benoitgrelard yes, calling checkValidity works and reports the status but it also triggers all radix validation messages. showing the messages is unwanted in this case and should only occur when calling reportValidity.

Edit: Here's a codepen showing the difference with native behavior:
https://codepen.io/chriscoyier/pen/mOzgBZ

@benoitgrelard
Copy link
Contributor Author

@benoitgrelard yes, calling checkValidity works and reports the status but it also triggers all radix validation messages. showing the messages is unwanted in this case and should only occur when calling reportValidity.

Edit: Here's a codepen showing the difference with native behavior: https://codepen.io/chriscoyier/pen/mOzgBZ

I understand the difference, I just assumed it would work the same given we just build on top.
But reading MDN a bit more:

checkValidity()
Returns true if the element's child controls are subject to constraint validation and satisfy those constraints; returns false if some controls do not satisfy their constraints. Fires an event named invalid at any control that does not satisfy its constraints; such controls are considered invalid if the event is not canceled. It is up to the programmer to decide how to respond to false.

So, because we hook onto onInvalid the message shows. It looks like the events are cancellable, but we'd have to find a way to decide when to cancel them. I'm not sure that's trivial considering we don't know what caused them… 🤔

@slavanga
Copy link

slavanga commented Apr 24, 2023

@benoitgrelard ok, seems hard to make the native way work while using onInvalid.
what about a radix API that checks the validation status? maybe something like ValidityState but for the whole form.

@benoitgrelard
Copy link
Contributor Author

@benoitgrelard ok, seems hard to make the native way work while using onInvalid. what about a radix API that checks the validation status? maybe something like ValidityState but for the whole form.

Yeah will need to look into it, the way the API call fire events seems counter-intuitive, so not sure if I'm missing something or what. What is the use-case btw?

@slavanga
Copy link

@benoitgrelard my current use case: on a page with multiple forms i want to show a visual status indicator to the user once all fields on a form are filled and valid.

i tried checking the validation state of each form with a debounced onChange event that would call checkValidity. that's when i ran into the issue described above.

@jjenzz
Copy link
Contributor

jjenzz commented Apr 26, 2023

@slavanga something like this might work for now if that's what you need?

@slavanga
Copy link

@jjenzz the link does not work for me

@jjenzz
Copy link
Contributor

jjenzz commented Apr 27, 2023

@slavanga sorry, it seems codesandbox makes sandboxes private by default these days. the link should work now 🙏

@slavanga
Copy link

@jjenzz thank you for putting this together.
it works great! although accessing the form validity state via an API from the Form primitive would be much more convenient.

@jjenzz
Copy link
Contributor

jjenzz commented Apr 28, 2023

agreed, an onValidityChange prop that internally does something like my sandbox might be nice :)

@snelsi
Copy link

snelsi commented Jun 12, 2023

Have been using the Form component for a while now, and it's been working fine.

There are two very critical points, which I hope will be fixed before the release:

  1. No possibility to use Radix.Checkbox and Radix.Select primitives together with Form.Control
  2. No built-in solutions/docs for working with arrays

@benoitgrelard Are there any plans/estimates for these two issues?

@benoitgrelard
Copy link
Contributor Author

@snelsi, definitely planning to work on it yet. No estimates at the moment I'm afraid.

@mattandryc
Copy link
Contributor

@benoitgrelard I noticed that error messages are linked to the input via aria-describedby at this point. Is there a reason for using aria-describedby over aria-errormessage here?

Screen Shot 2023-06-20 at 9 15 55 PM

@kentcdodds
Copy link

Sadly, support for that aria attribute is lacking, so it's best to use describedby

@jkohlin
Copy link

jkohlin commented Jun 28, 2023

Radix.Select need to be able to get an id attribute from Form.Control, or at least be able to set an id attribute to Radix.Select

nicholaschiang added a commit to nicholaschiang/dolce that referenced this pull request Jul 22, 2023
This patch adds a new progressively enhanced login form using `shadcn`
components and my own very simple style wrapper around the Radix
`<Form>` primitive. I'm using `conform-to` for server-side and
client-side error surfacing with `zod` for schema validation.

This login form is progressively enhanced, meaning that form validation
works the same both with and without JS enabled client-side. If JS is
enabled client-side, that just improves performance by checking form
validation client-side before running those same checks server-side.

See: radix-ui/primitives#1977 (comment)
See: https://github.com/rphlmr/remix-radix-form/blob/main/app/routes/with-conform.tsx
See: https://conform.guide/
See: https://www.radix-ui.com/docs/primitives/components/form#server-side-validation
See: https://ui.shadcn.com/docs/components/input#with-label

Closes: NC-630
nicholaschiang added a commit to nicholaschiang/dolce that referenced this pull request Jul 22, 2023
This patch adds a new progressively enhanced login form using `shadcn`
components and my own very simple style wrapper around the Radix
`<Form>` primitive. I'm using `conform-to` for server-side and
client-side error surfacing with `zod` for schema validation.

This login form is progressively enhanced, meaning that form validation
works the same both with and without JS enabled client-side. If JS is
enabled client-side, that just improves performance by checking form
validation client-side before running those same checks server-side.

See: radix-ui/primitives#1977 (comment)
See: https://github.com/rphlmr/remix-radix-form/blob/main/app/routes/with-conform.tsx
See: https://conform.guide/
See: https://www.radix-ui.com/docs/primitives/components/form#server-side-validation
See: https://ui.shadcn.com/docs/components/input#with-label

Closes: NC-630
@chaance chaance closed this Sep 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.