fabian-hiller / modular-forms

The modular and type-safe form library for SolidJS, Qwik and Preact
https://modularforms.dev
MIT License
1.05k stars 55 forks source link

[Question] Can it run with SSR? #3

Open EuSouVoce opened 2 years ago

EuSouVoce commented 2 years ago

It would be awsome if it runs and validates on the server side too...

snowfluke commented 1 year ago

Hi! It's nice to see the discussion, I just want to share some implementation that I did using SolidStart in SSR. @fabian-hiller thank you for the awesome libs!!

import { SubmitHandler, createForm } from "@modular-forms/solid";
import { createServerAction$ } from "solid-start/server";

type SomeForm = {
  title: string;
  file: File | undefined;
};

export default function MyForm() {
  const [_, Publish] = createServerAction$(async (formData: FormData) => {
    console.log(formData);
  });
  const [__, { Form, Field }] = createForm<SomeForm>();

  const handleSubmit: SubmitHandler<SomeForm> = async (values: {
    [key: string]: any;
  }) => {
    try {
      const body = new FormData();
      for (let key in values) {
        body.append(key, values[key]);
      }
      // Submit the action
      await Publish(body);
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <Form onSubmit={handleSubmit} enctype="multipart/form-data">
      {/* Your Form */}
    </Form>
  );
}
apollo79 commented 1 year ago

@snowfluke yes it's possible to use modular-forms with SSR, the question is more about ergonomic validation. Like your form would work without JS enabled in the browser if you would add action={Publish.url}, but validation on form values from modular-forms vs FormData on the server can be different (I think so at least 😅). Of course one could always validate on FormData I guess. I don't even know how this works for the other libraries modular-forms supports, gotta take a look.

apollo79 commented 1 year ago

Ah I see that one has to specify fields with special data types. I think the main blocker for built-in progressively enhanced forms for solid is still the extensibility of solid-start's APIs, as mentioned earlier.

snowfluke commented 1 year ago

Idk man, my head hurt and the docs is still lacking need to wait for 1.0.

brandonpittman commented 1 year ago

I know there's no specific support for server validation with React, but is there anyway to pass the server's response back into Modular Forms? Conform does this thing where you can get an action response from Remix and pass that to the useForm hook. It populates the form's state.

fabian-hiller commented 1 year ago

Doesn't the form hold the state? If it is about the state at initialization, you can use initialValues for the form values.

brandonpittman commented 1 year ago

Doesn't the form hold the state? If it is about the state at initialization, you can use initialValues for the form values.

I guess initialValues would work. I'm thinking about how to validate on the server and return the form state to the client from an action.

brandonpittman commented 1 year ago

Big fan of Modular Forms with Qwik so I'm trying to use it with React to make a later Remix => Qwik City migration easier.

It would seem like there's no way to avoid the built-in onSubmit handler. I'm wanting to have the form validation and then if it passes just do the normal browser submission if I don't pass my own onSubmit handler to the form.

https://github.com/fabian-hiller/modular-forms/blob/23b619d851e6361c86eda6a0bb8b93715bb57fe7/packages/react/src/components/Form.tsx#L74-L76

fabian-hiller commented 1 year ago

The problem is that Modular Forms for React is a bit experimental due to the Preact Signals. For example, it doesn't currently work with Next.js. Also, Signals are still mostly unfamiliar to React developers. So I'm not sure about the future of Modular Forms for React yet.

brandonpittman commented 1 year ago

Yeah, I kept thinking about it and because Modular Forms requires you to use its own <Form /> component, you can't use Remix's <Form /> component and it probably can't work the way I'd like it to.

fabian-hiller commented 1 year ago

I think that Modular Forms does not require that. You can also use useFormStore with a custom Form component.

brandonpittman commented 1 year ago

So pass the useFormStore value to as of, copy most of the onSubmit hook from <Form />, and then disable the preventDefault it might work.

fabian-hiller commented 1 year ago

Yes, that could work. Feel free to try it out. In the end, the individual data from useFormStore are simply held in signals.

brandonpittman commented 1 year ago

Would there be any problems with using Modular Forms' <Field /> components without Modular Forms' <Form /> component—as long as you pass the form state to of?

brandonpittman commented 1 year ago

I managed to get this working my copying most of the Form component's onSubmit handler and modifying it to play nice with Remix's usual behavior on the client.

The last problems are:

fabian-hiller commented 1 year ago

For the FormData and server stuff you can check how I solved it for Qwik. The Field component does not require our Form component. The only thing that is needed in most cases ist our store object. You can also write me on Discord if that is easier for you.

frenzzy commented 5 months ago

What if createForm accepts a Submission object and automatically extracts SSR input values and submission errors?

import { action, useSubmission } from '@solidjs/router'
import { createForm, required, email, minLength } from 'modular-forms'
import * as v from 'valibot'

const LoginSchema = v.object({
  email: v.pipe(v.string(), v.email()),
  password: v.pipe(v.string(), v.minLength(8)),
})

type LoginForm = v.InferOutput(typeof LoginSchema)

const login = action(async (fromData: FormData) => {
  'use server'
  const data = v.parseFormData(LoginSchema, formData) // throws FormError if necessary
  console.log(`Login with email "${data.email}" and password ${data.password}`)
})

const LoginPage = () => {
  const submission = useSubmission(login)
  const [loginForm, { Form, Field }] = createForm<LoginForm>({
    submission,
    initialValues: {
      email: 'user@email.com' // uses submission.input[0].get('email') || initialValues.email for initial state under the hood
    }
  })
  return (
    <Form> // action={submission.url} and method="post" are set by default
      <Field
        name="email"
        validate={[
          required('Please enter your email.'),
          email('The email address is badly formatted.'),
        ]}
      >
        {(field, props) => (
          <input
            {...props}
            value={field.value || ''}
            type="email"
            placeholder="Email"
            required
            prop:setCustomValidity={field.error || ''} // extracts error message from submission.error
          />
        )}
      </Field>
      <Field
        name="password"
        validate={[
          required('Please enter your password.'),
          minLength(8, 'You password must have 8 characters or more.'),
        ]}
      >
        {(field, props) => (
          <input
            {...props}
            value={field.value || ''}
            type="password"
            placeholder="Password"
            required
            prop:setCustomValidity={field.error || ''} // extracts error message from submission.error
          />
        )}
      </Field>
      <button type="submit">Login</button>
    </Form>
  )
}
fabian-hiller commented 5 months ago

I am still focused on Valibot and cannot work on this at the moment. Sorry about that!