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...

fabian-hiller commented 2 years ago

Yes, SSR works. Since our only dependency is SolidJS, you can pre-render the form on the server. However, validation only works on the client so far. Whether it works with streaming, I'm not sure. On the server I mainly use Zod to validate the incoming data.

Is your question also aimed at integrating SolidStart's createRouteAction API, so that if in doubt, the app will work without JavaScript in the browser?

EuSouVoce commented 2 years ago

Thanks! That was the precise question!

EuSouVoce commented 2 years ago

Currently i'm using remixjs, but SolidStart is great! And i'm starting to migrate. But i'm concerned 'bout the ssr validation on forms.

fabian-hiller commented 2 years ago

Since the documentation for SolidStart was not yet ready at the time of the development of this library, I could unfortunately not yet consider createRouteAction. But I will inform myself in the next weeks and check if an integration is possible and reasonable.

Did you also use a form library with Remix? How did the validation work there?

EuSouVoce commented 2 years ago

In Remix, I would use a Fetcher which is a component that self-updates fetching data from routes and re-renders the updates on itself.

fabian-hiller commented 2 years ago

Thank you for the information. I will give you a first assessment by next week.

thibgl commented 2 years ago

Hi, I am trying to use Modular Forms with Solid Start (not sure this is the best place to ask - noob here), I am using the example snippet from https://modularforms.dev/guides/validate-your-fields, which is working fine on my Solid (not Start) App.

The component is showing correctly, but when I type text in the inputs, the validation acts as if I typed nothing.

reactivity.ts

import { createForm } from "@modular-forms/solid"

type LoginForm = {
  email: string
  password: string
}

export const createLogin = () => {
  const loginForm = createForm<LoginForm>()
  return loginForm
}

component.tsx

import { createLogin } from "./reactivity"

interface Props extends ReturnType<typeof createLogin> {}

const Login: Component<Props> = (props) => {
return (
  <Form of={props}...>
    <Field of={props}...>{(field) => (<input>...</input>)}</Field>
    ...
  </Form>)}

export default Login

app index.tsx

import Login, { createLogin } from "@components/login"

export default function () {
  const login = createLogin()
  return (
    <>
      <Login {...login} />
    </>
  )
}

Thanks for your help and sorry if the question is misplaced.

Cheers

fabian-hiller commented 2 years ago

Hi @thibaudgrosjean. The problem could be that you are using the spread operator at <Login {...login} />. This can break the fine-graind reacktivity. It doesn't matter if you use SolidJS with or without SolidStart. For example, our documentation is written with SolidStart.

If the problem persists, I recommend putting the code into one component and then piece by piece splitting it up again until you find the problem. Feel free to give me an update here. I am happy to help.

fabian-hiller commented 2 years ago

You can also join the SolidJS Discord server and contact me there: https://discord.com/invite/solidjs

thibgl commented 2 years ago

Hey Fabian, I must be doing something wrong (I'll check this spread thingy out), I have tried putting the code in a single file but still got the same output. I will study your doc source code, I might learn a few things:D Thanks for the heads up !

thibgl commented 2 years ago

Hi, so I was having the issue on the bat template (https://github.com/olgam4/bat) but using the Solid Start templates (tried both with and without SSR) it is indeed working fine, I guess it has something to do with the bat template config, anyway, have a nice day:)

fabian-hiller commented 2 years ago

In the end, this library only uses SolidJS and the browser API. There should only be problems if the required JavaScript code is not passed to the browser. For example because it is an MPA instead of an SPA. If you are interested, you can provide me a GitHub repository with your code and I'll take a closer look.

fabian-hiller commented 1 year ago

@EuSouVoce I have looked at createRouteAction and createServerAction$ from SolidStart. An integration seems to me possible without big changes. However, I need to go deeper into SolidStart for this.

Currently, my idea is to add a createActionForm and createServerForm$ primitive to Modular Forms that internally use createRouteAction and createServerAction$ from SolidStart. The code in the JSX section should not be affected and createForm, createActionForm and createServerForm$ should be able to be interchanged as desired. This means the API will remain the same according to my current estimation.

The only difference to the current implementation will be, that the submit function will have to be passed directly to createActionForm or createServerForm$. Also, it will be necessary to add optional schema validation, e.g. using Zod, which can also be run on the server. I plan to add the schema validation in the next days.

fabian-hiller commented 1 year ago

The first step is done. With v0.9 it is now possible to validate the form with a Zod schema. You can find more about this here.

EuSouVoce commented 1 year ago

AWSOME!!!!

apollo79 commented 1 year ago

Is there progress on this? I want to use modular forms for a site I am building and would need this feature.

fabian-hiller commented 1 year ago

I answered this question in the FAQ of the website a few hours ago. Basically you can use Modular Forms with SolidStart. I'm doing this myself on a larger project. The integration of SolidStart actions is only interesting if your forms need to work without JavaScript in the browser or you are developing an MPA and don't want to send JavaScript to the browser at all. Otherwise, it makes no difference to your end users.

At this point, I can only make assumptions about the final implementation. However, if everything goes according to my plan, it should be possible to implement Modular Forms as described in the current documentation, and once the new APIs with the SolidStart actions are available, you simply swap them out and make a few small changes.

apollo79 commented 1 year ago

The integration of SolidStart actions is only interesting if your forms need to work without JavaScript in the browser

Exactly that ist the usecase. I need it to work without client JS at all. So the form has to be rendered with values and error messages server side.

However the project will hopefully go life in one or two months. Will it be ready then?

fabian-hiller commented 1 year ago

Yes, that's the plan. However, I need to dig deeper into how the SolidStart actions work. Especially how to get the error messages into the HTML. Also I'm still unsure if I can simply build on createServerAction$ for example, as I've made the experience that server$ only works in the routes directory. However, I could be wrong with this. I'll try to give you an update here by the end of the week.

apollo79 commented 1 year ago

There is the FormError class of solid-start, which is designed for these usecases. But errors are redirected to the referrer with the error as URL parameter. For big forms this might be a problem, as the maximum URL length is about 4000 characters afaik, which might be too less for values and error messages.

apollo79 commented 1 year ago

I can only think of a session as alternative for holding the error messages and values for a redirect. But that might be difficult for a form library...

fabian-hiller commented 1 year ago

I dug a little deeper and also investigated what approach Remix has. My current guess, as you mentioned, is that with SolidStart the entire form state needs to be added to the URL as a parameter or a cookie so that server-side the page can be re-rendered with the previous form values and optionally the error messages. If there is a limit on the URL or cookie length, however, that seems too error-prone to me. I will ask for advice on this in the SolidJS Discord.

During my tests I also encountered other possible problems. For example, if I use createRouteAction and disable JavaScript in the browser and submit the form, only the page reloads and nothing else happens. In this case, I expected the data to be processed automatically on the server side. Another problem I encountered is that createServerAction$ behaves like createRouteAction and is executed client-side when defined outside the route in which it is used. This causes problems for us as a library, as it means that we cannot currently build on createServerAction$ in our code.

So in summary, I would say that because of the current problems, it is not possible to predict when we will be able to offer a satisfactory solution for progressively enhanced forms. All currently possible implementations seem to me to involve too much boilerplate code for the users of the library. Below is an example:

import {
  createForm,
  Field,
  handleActionSubmit,
  handleFormAction,
  zodForm,
} from "@modular-forms/solid";
import { useSearchParams } from "@solidjs/router";
import { createServerAction$ } from "solid-start/server";
import { z } from "zod";
import { TextInput, Button } from "~/components";

const loginSchema = z.object({
  email: z
    .string()
    .min(1, "Please enter your email.")
    .email("The email address is badly formatted."),
  password: z
    .string()
    .min(1, "Please enter your password.")
    .min(8, "You password must have 8 characters or more."),
});

export default function LoginRoute() {
  const [searchParams] = useSearchParams();

  const loginForm = createForm<z.infer<typeof loginSchema>>({
    validate: zodForm(loginSchema),
    state: searchParams.formState,
  });

  const [_, loginAction] = createServerAction$(async (formData: FormData) =>
    handleFormAction(loginForm, formData, async (values) => {
      // Your code
    })
  );

  return (
    <loginAction.Form
      onSubmit={handleActionSubmit(loginForm, loginAction)}
      noValidate
    >
      <Field of={loginForm} name="email">
        {(field) => (
          <TextInput
            {...field.props}
            type="email"
            label="Email"
            value={field.value}
            error={field.error}
            required
          />
        )}
      </Field>
      <Field of={loginForm} name="password">
        {(field) => (
          <TextInput
            {...field.props}
            type="password"
            label="Password"
            value={field.value}
            error={field.error}
            required
          />
        )}
      </Field>
      <Button type="submit">Login</Button>
    </loginAction.Form>
  );
}

The ideal implementation, on the other hand, would look something like this:

import { createServerForm$, Field, zodForm } from "@modular-forms/solid";
import { z } from "zod";
import { TextInput, Button } from "~/components";

const loginSchema = z.object({
  email: z
    .string()
    .min(1, "Please enter your email.")
    .email("The email address is badly formatted."),
  password: z
    .string()
    .min(1, "Please enter your password.")
    .min(8, "You password must have 8 characters or more."),
});

export default function LoginRoute() {
  const [loginForm, Form] = createServerForm$<z.infer<typeof loginSchema>>({
    validate: zodForm(loginSchema),
    onSubmit: async (values) => {
      // Your code
    },
  });

  return (
    <Form>
      <Field of={loginForm} name="email">
        {(field) => (
          <TextInput
            {...field.props}
            type="email"
            label="Email"
            value={field.value}
            error={field.error}
            required
          />
        )}
      </Field>
      <Field of={loginForm} name="password">
        {(field) => (
          <TextInput
            {...field.props}
            type="password"
            label="Password"
            value={field.value}
            error={field.error}
            required
          />
        )}
      </Field>
      <Button type="submit">Login</Button>
    </Form>
  );
}
apollo79 commented 1 year ago

I must say, that the more I dig into this, the more problems I see with validation in browsers with JS disabled. The client side validation is currently done with that object of type FieldValues. But server side validation would have to be done for FormData, as this is the type of data we get from a POST request from a form. There are a few problems with this:

So an additional question may be how to handle these differences. Add a layer to handle both formats? Or doing client side validation on FormData too? Do you have thoughts on this?

EuSouVoce commented 1 year ago

We are providing the form itself from the server, the formData object will be tested for validation and we already expect X amount of fields with the respective types.

apollo79 commented 1 year ago

We are providing the form itself from the server, the formData object will be tested for validation and we already expect X amount of fields with the respective types.

Can you elaborate this further? I am not sure if I understand it right. How would this solve the problem with different object shapes on server and client?

EuSouVoce commented 1 year ago

For example, if we create a login form, we will expect the given string to be an email, the checkbox as bool, the number as number/string (depending on the way implemented), etc.

apollo79 commented 1 year ago

I still don't get it 😅. This sounds basically just like validation. The problem I described was about how to use the same validation functionality for both server and client even if the given objects for these two different validations have not the same shape.

apollo79 commented 1 year ago

Another problem I encountered is that createServerAction$ behaves like createRouteAction and is executed client-side when defined outside the route in which it is used. This causes problems for us as a library, as it means that we cannot currently build on createServerAction$ in our code.

Do you know if there is a reason for that? I think, the urls for the actions are generated from the route where they are called in. Maybe that's the cause? If not, I think, we should open an issue. And is there a workaround, like using a combination of createRouteAction and server$? For what would we need this anyway? The forms are created in the routes, so it should work, I think?

fabian-hiller commented 1 year ago

@apollo79 thank you for your comment regarding the FormData object. I will continue to look around and also wait to see how SolidStart develops. Maybe a form library that is supposed to work in conjunction with SolidStart without JavaScript needs a completely different approach. It might also make more sense to provide an @modular-forms/solid-start package that works a little differently under the hood.

apollo79 commented 1 year ago

Did you find anything interesting in the last days? I could also think of an approach using a solid-start middleware maybe, because server actions aren't really extendable as far as I see. But that would of course come with different problems like how to register actions. (I am just writing down my thoughts here so we can think of all possible options)

apollo79 commented 1 year ago

Another interesting thing I saw, which would perhaps make it possible to get a similar behaviour to createServerAction$ are the possibilities with the basic server$ function. Its registerHandler method might be really helpful.

fabian-hiller commented 1 year ago

Yes, I have a plan to make it work with createServerAction$. It comes with a bit of boilerplate code, but for now it seems like the only possible option. From there we need to talk to the SolidStart team to improve the DX for both sides.

What exactly is your idea with server$? As far as I know, the server$ functions are called via the Fetch API. This would mean that JavaScript is required in the browser for this.

apollo79 commented 1 year ago

What exactly is your idea with server$? As far as I know, the server$ functions are called via the Fetch API. This would mean that JavaScript is required in the browser for this.

It doesn't need JS. Submitting a form to the route where it is registered also works. Example:

import { type VoidComponent } from "solid-js";
import { isServer } from "solid-js/web";
import server$, { redirect } from "solid-start/server";

if (isServer) {
  server$.registerHandler("/hello", (form: FormData, { request }: {request: Request}) => {   
    const response = redirect("/");

    // set session header
    // do stuff here...

    return response;
  });
}

const Home: VoidComponent = () => {
  return (
      <main>
        <form action="/hello" method="post">
          <input type="text" value="hello" />
          <button type="submit">Submit</button>
        </form>
      </main>
  );
};

export default Home;

This works even with JS completely disabled. I thought this could be helpful.

fabian-hiller commented 1 year ago

Thanks for the information! I will take that into consideration. I will probably start the implementation on Thursday or Friday. I will be happy to present my solution here so that we can improve the DX together.

apollo79 commented 1 year ago

Thanks for the information! I will take that into consideration. I will probably start the implementation on Thursday or Friday. I will be happy to present my solution here so that we can improve the DX together.

Nice! I look forward to it :-)

fabian-hiller commented 1 year ago

I just want to let you know that I probably won't be able to implement it until next week or the week after that. I'll give an update here then.

apollo79 commented 1 year ago

I just want to let you know that I probably won't be able to implement it until next week or the week after that. I'll give an update here then.

No problem! 👍

fabian-hiller commented 1 year ago

I have more time again in the next few weeks and will actually get to work on the implementation soon.

fabian-hiller commented 1 year ago

I have been implementing Modular Forms for Qwik for the last two weeks and also experimenting with actions. The implementation for SolidJS is still pending, but I have already published the documentation of the API to be expected on our website. As soon as there is more news about it, I will report here. https://modularforms.dev/guides/handle-submission

apollo79 commented 1 year ago

Awesome! :rocket:

awojciak commented 1 year ago

Hi, I'm getting this error on SolidStart when a page with form is the first one rendered: image What might have caused it? Are there any extra steps needed when using this package with SolidStart SSR?

awojciak commented 1 year ago

also this happens when trying to build app bundle: image

fabian-hiller commented 1 year ago

Hi @awojciak, I see this problems for the first time. No, there are no special steps required. Have you installed Modular Forms via npm? Does everything work when you disable SSR? Can you share your code with me via GitHub or StackBlitz? Then I could try to help you.

awojciak commented 1 year ago

@fabian-hiller 1) I've used yarn 2) yes, build works when ssr is disabled 3) no, I'm working in a private repo

fabian-hiller commented 1 year ago

Ok, yarn should not be the problem. I myself use pnpm for the Modular Forms website. For the website the build step with SSR works without any problems. So I suspect the problem is more with your setup, also because it's an import issues, on which I have no influence as a library. Without code or more info I can't help you though. If you want, you can create a new SolidStart project and try to reproduce the problem with minimal code. Then I could look over it and investigate the problem.

awojciak commented 1 year ago

ok, I've found a solution - I had to add this to vite config: ssr: { noExternal: ['@modular-forms/solid'] },

fabian-hiller commented 1 year ago

Thank you for taking the time to share your solution with us. If this happens again with another user, I will add it to the FAQ.

fabian-hiller commented 1 year ago

Hello everyone, I would like to give an update on this issue. With Modular Forms for Qwik I was able to fully integrate actions. This is due to the fact that with Qwik I could build on globalAction$ without any problems to provide a custom formAction$ API. With Solid Start this is currently not possible, but it is planned. So I prefer to wait until more information about this is published.

brandonpittman commented 1 year ago

Not sure if this would be of interest, but the Conform library has done a good job of solving this problem for React and Remix.

http://conform.guide

fabian-hiller commented 1 year ago

Thank you for the tip!