Validate your fields

During the development of Modular Forms, we paid special attention to input validation. It is one of the core functionalities of the library. You can use our internal validation functions or optionally a schema library such as Valibot or Zod.

To keep the bundle size small, validation is optional and modular. If your form requires validation, just import the validation functions you need. Another advantage is that you can define the validation globally for the whole form or fine-granularly for each field individually. You can even combine the both methods.

Note that the validation of Modular Forms, except for server actions, takes place in the browser and can be manipulated by the user. Make sure that the values are validated again on the server before you process them or store them in your database.

Validation functions

With our internal validation functions you can reduce your bundle size to a minimum and also achieve unprecedented performance. You can validate almost anything with one line of code. Be it an email, URL or the MIME type of a file.

With our validation functions you define the validation next to your fields via the validate property. Thus, things are together that belong together. This has a big impact on the DX.

Login form example

In the following example we first use required to make the input mandatory and then email to check the formatting and minLength to check the number of characters.

Note that if you do not add required, the input will only be validated if a value is present. In this case an empty string or list will not cause an error.

Also you have the possibility to set your own error message and access the current error via the render function of the Field component. An overview of all validation functions can be found in our API reference.

import { component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import {
  email,
  type InitialValues,
  minLength,
  required,
  useForm,
} from '@modular-forms/qwik';

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

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

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

  return (
    <Form>
      <Field
        name="email"
        validate={[
          required<string>('Please enter your email.'),
          email('The email address is badly formatted.'),
        ]}
      >
        {(field, props) => (
          <>
            <input {...props} type="email" required />
            {field.error && <div>{field.error}</div>}
          </>
        )}
      </Field>
      <Field
        name="password"
        validate={[
          required<string>('Please enter your password.'),
          minLength(8, 'You password must have 8 characters or more.'),
        ]}
      >
        {(field, props) => (
          <>
            <input {...props} type="password" required />
            {field.error && <div>{field.error}</div>}
          </>
        )}
      </Field>
      <button type="submit">Login</button>
    </Form>
  );
});

Times of validation

By default, the first validation is done when the form is submitted for the first time and from there on, a revalidation is triggered after each user input. You can change this behavior using the validateOn and revalidateOn option of the useForm hook.

Schema validation

For maximum flexibility, you can also use your familiar schema library for form validation. We currently offer adapters for Valibot and Zod. Please create an issue if you want us to add more adapters.

Valibot fits especially well with Modular Forms, since the library is also modular. Valibot adds about 700 bytes to your JavaScript bundle for the following schema, whereas other schema libraries usually start at more than 10 KB.

Use valiForm$ to define the validation for the whole form and valiField$ for a single field. Below is the same login form as in the example above. Instead of our validation functions, Valibot is used now.

import { $, component$ } from '@builder.io/qwik';
import { routeLoader$ } from '@builder.io/qwik-city';
import { type InitialValues, useForm, valiForm$ } from '@modular-forms/qwik';
import * as v from 'valibot';

const LoginSchema = v.object({
  email: v.pipe(
    v.string(),
    v.nonEmpty('Please enter your email.'),
    v.email('The email address is badly formatted.')
  ),
  password: v.pipe(
    v.string(),
    v.nonEmpty('Please enter your password.'),
    v.minLength(8, 'You password must have 8 characters or more.')
  ),
});

type LoginForm = v.InferInput<typeof LoginSchema>;

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

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

  return (
    <Form>
      <Field name="email">
        {(field, props) => (
          <>
            <input {...props} type="email" required />
            {field.error && <div>{field.error}</div>}
          </>
        )}
      </Field>
      <Field name="password">
        {(field, props) => (
          <>
            <input {...props} type="password" required />
            {field.error && <div>{field.error}</div>}
          </>
        )}
      </Field>
      <button type="submit">Login</button>
    </Form>
  );
});