Create your form

Modular Forms consists of hooks, components and methods. To create a form you use the useForm hook.

Initial values

In Qwik, a form starts with its initial values. With this information we can initialize the store of your form and enable Qwik to reliably start rendering your website on the server. We also use the initial values later to check if the value of a field has changed.

import { routeLoader$ } from '@builder.io/qwik-city';
import { type InitialValues } from '@modular-forms/qwik';

type LoginForm = {
  email: string;
  password: string;
};

export const useFormLoader = routeLoader$<InitialValues<LoginForm>>(() => {
  return {
    email: '',
    password: '',
  };
});

Dynamic values

If your initial values are dynamic, we recommend using routeLoader$. The code of your routeLoader$ will only be executed server-side, which makes it perfect for retrieving the initial data of your form from a database, for example.

Static values

For static values you can also use routeLoader$. However, if your form is a bit deeper in the component tree, you can use useSignal instead or just a normal JavaScript object where the initial values are available under a value key.

const [loginForm, { Form, Field }] = useForm<LoginForm>({
  loader: { value: { email: '', password: '' } },
});

It is planned to improve the DX for static values in the future. If you have any ideas, we would be happy to hear from you here.

Form hook

The initial values are followed by the useForm hook, to which you then pass your loader. The hook returns a tuple containing the store of the form in the first place and an object with the Form, Field and FieldArray component afterwards. You can use the store to access the state of the form and make changes to it using methods that will be introduced later.

export default component$(() => {
  const [loginForm, { Form, Field, FieldArray }] = useForm<LoginForm>({
    loader: useFormLoader(),
  });
});

Destructuring

You do not have to destructure the object with the components. In this way, the tuple allows you to freely name the two return values. This can be useful, for example, when a page contains multiple forms. Also, you can simply omit the first return value if you don't need it.

export default component$(() => {
  const [, Login] = useForm<LoginForm>({ loader: useLoginLoader() });
  const [, Register] = useForm<RegisterForm>({ loader: useRegisterLoader() });

  return (
    <>
      <Login.Form></Login.Form>
      <Register.Form></Register.Form>
    </>
  );
});

Components

The returned Form, Field and FieldArray component are already connected with the store of your form. Also, if you are using TypeScript, they are aware of your fields and their data types, which gives you the benefit of type safety and autocompletion.

JSX code

In the JSX part of your component you start with the Form component. It encloses the fields of your form and through its properties you can define what happens when the form is submitted. More on this later.

export default component$(() => {
  const [loginForm, { Form, Field }] = useForm<LoginForm>({
    loader: useFormLoader(),
  });

  return <Form></Form>;
});