Complex Forms

What are Complex Forms?

Complex forms would typically contain data in a structure that is more than just simple key/value pairs. For example, a list of people who each have their own email, phone and street address. This would essentially be an array of objects.

Input naming

Objects

Nested items can be created by separating the items with . (dot) in the name. For example, person.name would be converted to { person: { name: 'sam' } }.

Arrays

Arrays, either top level or nested are created by specifying the zero-based index in the name. For example person.pets.0 would be converted to { person: { pets: [ 'cat' ] } }.

Example

The key to creating a complex form is in the naming of the inputs. Below would be an example of the list described above:

<form>
  <input name="person.0.name" value="Sam">
  <input name="person.0.email" value="sam@complexform.com">
  <input name="person.0.pets.0" value="cat">
  <input name="person.0.pets.1" value="dog">
  <input name="person.0.address.street" value="1234 Example Ave.">
  <input name="person.0.address.city" value="Qwik">
  <input name="person.0.address.state" value="IA">
  <input name="person.0.address.zip" value="00000">
  <input name="person.0.pets.0" value="beaver">
 
  <input name="person.1.name" value="Bonnie">
  <input name="person.1.email" value="bonnie@hishai.net">
  <input name="person.1.address.street" value="768 Resolution Way">
  <input name="person.1.address.city" value="Jaffa">
  <input name="person.1.address.state" value="IL">
  <input name="person.1.address.zip" value="01948">
</form>

Output object

The after submitting the form the data would be parsed in an object like this:

{
  "person": [
    {
      "name": "Sam",
      "email": "sam@complexform.com",
      "address": {
        "street": "1234 Example Ave.",
        "city": "Qwik",
        "state": "IA",
        "zip": "00000"
      },
      "pets": ["beaver"]
    },
    {
      "name": "Bonnie",
      "email": "bonnie@hishai.net",
      "address": {
        "street": "768 Resolution Way",
        "city": "Jaffa",
        "state": "IL",
        "zip": "01948"
      }
    }
  ]
}

Using with Actions

Complex forms can be validated using zod$ with routeAction$ and globalAction$. Continuing with the previous examples it would look like this:

export const action = routeAction$(
  async (person) => {
    return { success: true, person, };
  },
  // Zod schema is used to validate the FormData
  zod$({
    person: z.array(
      z.object({
        name: z.string(),
        email: z.string().email(),
        address: z.object({
          street: z.string(),
          city: z.string(),
          state: z.string(),
          zip: z.coerce.number()
        }),
        pets: z.array(z.string())
      })
    ),
  })
);

Field errors also in dot notation (almost)

If you use dot notation, the error messages are also returned in dot notation in the fieldErrors property. This has the advantage that the input name and fieldError key match.

For the this action:

export const addPersonAction = routeAction$(
  async person => {
    return { success: true, person };
  },
  // Zod schema is used to validate the FormData
  zod$({
    person: z.object({
      name: z.string(),
      email: z.string().email(),
      address: z.object({
        street: z.string(),
        city: z.string(),
        state: z.string(),
        zip: z.coerce.number(),
      }),
      pets: z.array(z.string()),
    }),
  })
);

You would get this kind of fieldErrors if all are wrong:

{
  "person.name": "Invalid string",
  "person.email": "Invalid email",
  "person.address.street": "Invalid string",
  "person.address.city": "Invalid string",
  "person.address.state": "Invalid string",
  "person.address.zip": "Invalid number",
  "person.pets[]": ["Required"]
}

If you make person an array, the error messages will switch to a slightly different notation as:

{
  "person[].name": ["Invalid string"],
  "person[].email": ["Invalid email"],
  "person[].address.street": ["Invalid string"],
  "person[].address.city": ["Invalid string"],
  "person[].address.state": ["Invalid string"],
  "person[].address.zip": ["Invalid number"],
  "person[].pets[]": ["Required"]
}

If you completely forget to assign a value to an array type, lets say you completely forget person in the form. The error is then in fieldErrors["person[]"].

This way you can easily match the error messages to the input names like this:

export const useAddPersonAction = routeAction$(
  async person => {
    console.log(person);
    return { success: true, person };
  },
  zod$({
    person: z.object({
      name: z.string().min(2),
      email: z.string().email(),
      address: z.object({
        street: z.string().min(2),
        city: z.string().min(2),
        state: z.string().min(2),
        zip: z.coerce.number(),
      }),
      pets: z.array(z.string().min(2)),
    }),
  })
);
 
export default component$(() => {
  const testAction = useAddPersonAction();
 
  const renderError = (errorMessage: string | undefined) => {
    if (!errorMessage) return null;
    return <p class="error">{errorMessage}</p>;
  };
 
  return (
    <Form action={testAction}>
      <input type="email" name="person.email" placeholder="Email" />
      {renderError(testAction.value?.fieldErrors?.["person.email"])}
      <input type="text" name="person.name" placeholder="Name" />
      {renderError(testAction.value?.fieldErrors?.["person.name"])}
      <input type="text" name="person.address.street" placeholder="Street" />
      {renderError(testAction.value?.fieldErrors?.["person.address.street"])}
      <input type="text" name="person.address.city" placeholder="City" />
      {renderError(testAction.value?.fieldErrors?.["person.address.city"])}
      <input type="text" name="person.address.state" placeholder="State" />
      {renderError(testAction.value?.fieldErrors?.["person.address.state"])}
      <input type="text" name="person.address.zip" placeholder="Zip" />
      {renderError(testAction.value?.fieldErrors?.["person.address.zip"])}
      <input type="text" name="person.pets.0" placeholder="Pet 1" />
      {renderError(testAction.value?.fieldErrors?.["person.pets[]"]?.[0])}
      <button>Send</button>
    </Form>
  );
});

For this example the fieldErrors look like this:

{
  "person.name": "String must contain at least 2 character(s)",
  "person.email": "Invalid email",
  "person.address.street": "String must contain at least 2 character(s)",
  "person.address.city": "String must contain at least 2 character(s)",
  "person.address.state": "String must contain at least 2 character(s)",
  "person.pets[]": [
    "String must contain at least 2 character(s)"
  ]
}

Contributors

Thanks to all the contributors who have helped make this documentation better!

  • ulic75
  • hamatoyogi
  • aendel
  • tzdesign