vaadin / hilla

Build better business applications, faster. No more juggling REST endpoints or deciphering GraphQL queries. Hilla seamlessly connects Spring Boot and React to accelerate application development.
https://hilla.dev
Apache License 2.0
922 stars 58 forks source link

Form binding support for React Vaadin components #587

Open platosha opened 2 years ago

cromoteca commented 1 year ago

This is what I can see by building a simple form using Hilla, React, Formik, and Yup. Consider an endpoint like this one:

@Endpoint
@AnonymousAllowed
public class HelloFormEndpoint {

  @Nonnull
  public String validate(@Nonnull RegistrationInfo info) {
    return "Registration accepted";
  }

  public static record RegistrationInfo(
      @NotBlank String name,
      @NotBlank @Email String email,
      @Pattern(regexp = "^[0-9]+$") String phone,
      @Size(min = 2, max = 2) String country,
      @AssertTrue boolean conditions) {}
}

It can be used in this form:

image

<Formik
  initialValues={{ ...initialValues }}
  validationSchema={yupSchema}
  onSubmit={async values => {
    const response = await HelloFormEndpoint.validate(values);
    Notification.show(response, { theme: 'success' });
  }}
>
  {({ submitForm, isValid, dirty }) => (
    <Form>
      <VerticalLayout theme="spacing padding">
        <Field name="name">
          {({ field, meta }: any) => (
            <TextField {...field} placeholder="Name"
              invalid={meta.error && meta.touched} errorMessage={meta.error} />
          )}
        </Field>

        <Field name="email">
          {({ field, meta }: any) => (
            <TextField {...field} placeholder="Email"
              invalid={meta.error && meta.touched} errorMessage={meta.error} />
          )}
        </Field>

        <Field name="phone">
          {({ field, meta }: any) => (
            <TextField {...field} placeholder="Phone"
              invalid={meta.error && meta.touched} errorMessage={meta.error} />
          )}
        </Field>

        <Field name="country">
          {({ field, meta }: any) => (
            <Select {...field} placeholder="Country" items={countries} name={field.name}
              invalid={meta.error && meta.touched} errorMessage={meta.error} />
          )}
        </Field>

        <Field name="conditions">
          {({ field, meta }: any) =>
            <Checkbox {...field} label="I accept terms and conditions"
              invalid={meta.error && meta.touched} errorMessage={meta.error}
              onCheckedChanged={field.onChange} />
          }
        </Field>

        <Button theme="primary" onClick={submitForm} disabled={!(dirty && isValid)}>Submit</Button>
      </VerticalLayout>
    </Form>
  )}
</Formik>

Both initialValues and yupSchema could be generated, just like it happens in Lit (see #585):

const initialValues: RegistrationInfo = {
  name: '',
  email: '',
  phone: '',
  country: '',
  conditions: false,
};

const yupSchema: ObjectSchema<RegistrationInfo> = object({
  name: string().required(),
  email: string().email().required(),
  phone: string().matches(/^[0-9]+$/).required(),
  country: string().min(2).max(2).required(),
  conditions: boolean().oneOf([true]).required(),
});

Concerning fields, there's a clear pattern that could be formalized and implemented to replace them with something like <FormTextField/>, <FormSelect/>, <FormCheckbox/>, and so on. If they were one-liners, the form would be really compact:

<Formik
  initialValues={{ ...initialValues }}
  validationSchema={yupSchema}
  onSubmit={async values => {
    const response = await HelloFormEndpoint.validate(values);
    Notification.show(response, { theme: 'success' });
  }}
>
  {({ submitForm, isValid, dirty }) => (
    <Form>
      <VerticalLayout theme="spacing padding">
        <FormTextField name="name" placeholder="Name" />
        <FormTextField name="email" placeholder="Email" />
        <FormTextField name="phone" placeholder="Phone" />
        <FormSelect name="country" placeholder="Country" items={countries} />
        <FormCheckbox name="conditions" label="I accept terms and conditions" />

        <Button theme="primary" onClick={submitForm} disabled={!(dirty && isValid)}>Submit</Button>
      </VerticalLayout>
    </Form>
  )}
</Formik>
platosha commented 1 year ago

Let's consider a generic FormField instead of wrapping every component in existence as FormTextField and so on:

   <Form>
      <FormLayout>
        <FormField component={TextField} name="name" label="Name" />
        <FormField component={EmailField}  name="email" label="Email" />

        <Button theme="primary" onClick={submitForm} disabled={!(dirty && isValid)}>Submit</Button>
      </FormLayout>
    </Form>

This would then support both HTML and Vaadin React components, and even custom third-party components, for as long as they are based on Vaadin components or follow Vaadin conventions to some extent.

cromoteca commented 1 year ago

That would be great, but how do you handle special needs like the onCheckedChanged={field.onChange} I had to add to the Checkbox?

I also don't see the difference between our FormField and Formik's Field: you can do <Field as={TextField}.../>

platosha commented 1 year ago

I assume we can rely on events that work with all components, such as HTML standard onInput / onChange.

The Formik's Field does not have built-in support for invalid and errorMessage, this is something we could improve with our directive.

gitgmihd commented 1 year ago

Hello everyone!

I'm interested in the discussion on React Form support in Hilla and wanted to share my thoughts on the use of Formik for field validation. While Formik has been a popular choice for managing form state and validation in React, I noticed that its development activity seems to be slowing down - the last release (2.2.9) was back in June 2021.

In light of this, I'd like to suggest considering React Hook Form instead, as it's actively developed and provides an easy-to-use API for building forms in React. I found some helpful information on this topic in the 'Is Formik dead?' issue on GitHub (https://github.com/jaredpalmer/formik/discussions/3526).

What are your thoughts on this? Have you considered React Hook Form as an alternative to Formik for hilla? Thank you for your consideration.

cromoteca commented 1 year ago

Hello, yes, React Hook Form is definitely in the list of candidates. The link you cited confirms that it should get more attention than Formik.

marcushellberg commented 1 year ago

Should we change the title of this ticket until we decide which form library we plan to support?

platosha commented 1 year ago

Now that we have realised that Formik is not a sustainable choice, we have to pick some other library. While React Hook Form is a good candidate, the alternative that we are also considering right now is adapting the existing Hilla+Lit form binder to React.

Right now it seems that the complexity in the form binding feature is not in the management of the from state, lifecycle, and client-side validation (all libraries are reasonably good at this), but rather in integration with Vaadin components and Hilla endpoints, both of which use a number of custom conventions / APIs, that are not generally supported by third-party form libraries out-of-the-box. Reusing Hilla+Lit form binder is a quick way to address all these integration challenges (both this issue and the related validation one #585), with some additional benefits in the resulting solution.

I did some prototyping for the idea of using Hilla form binder with React and hooks. Here is the API I came up with in the prototype:

export default function FormView() {
  const { model, submit } = useBinder(EntityModel, {onSubmit: FormEndpoint.sendEntity});

  return (
    <>
      <section className="flex p-m gap-m items-baseline">
        <TextField label="Name" {...field(model.name)}></TextField>
        <ComboBox label="Choose" {...field(model.choice)} items={comboBoxItems}></ComboBox>
        <NumberField label="Number" {...field(model.number)}></NumberField>
        <DatePicker label="Date" {...field(model.date)}></DatePicker>
        <Button onClick={submit}>submit</Button>
      </section>
    </>
  );
}

The result is quite similar to React Hook Form. This is not a big surprise to me, as we took some inspiration from React hooks in the original Hilla+Lit form binder itself.

Please share your feedback about this idea and the API.

platosha commented 1 year ago

Let us use this issue as an epic for the other Hilla React binder implementation issues.