---
title: "Forms and Validation · RUAL Documentation"
description: "Checks the browser runs before anything is sent, server errors next to the right field with web_errors, and forms that keep working without JavaScript."
canonical: https://docs.rual.nl/web/forms
language: en
---

# Forms and Validation

A form with `data-action` checks itself in the browser before anything is sent. Your action checks what only the server can know and answers with [`web_errors`](https://docs.rual.nl/block-types/web/web_errors), which puts each message next to its field. A plain form keeps working without JavaScript.

```
<form data-action="subscribe">
  <label>Email <input name="email" type="email" required data-msg-required="{{t "form.email_required"}}"></label>
  <div data-error-for="email"></div>
  <div data-error-for="_form"></div>
  <button type="submit" data-loading-text="{{t "form.sending"}}">{{t "form.subscribe"}}</button>
</form>
```

## The browser checks first, without a request

A form with `data-action`, or with a submit button that has one, uses the checks HTML already has: `required`, `type="email"` and the other input types, `pattern`, `minlength`, `maxlength`, `min`, `max` and `step`.

- **On submit** every field is judged, every message appears next to its field, and the first bad field gets the focus. Only a form that passes reaches your action.

- **On leaving a field** that holds a bad value, its message appears.

- **While fixing a field** its message goes as soon as the value is valid. A message from your server goes too, the moment the visitor edits that field.

All of that costs no request. To turn it off for one form, add the standard `novalidate` attribute: the form then posts as it is and your action does all the checking.

### Write your own message per failed check

By default the visitor reads the browser's own message, in the browser's language. A `data-msg-*` attribute on the field replaces it.

| Attribute | Shown when |
| --- | --- |
| `data-msg-required` | A required field is empty. |
| `data-msg-type` | The value does not fit the input's type, such as an email without an at sign, or text in a number field. |
| `data-msg-pattern` | The value does not match `pattern`. |
| `data-msg-minlength`, `data-msg-maxlength` | The value is too short or too long. |
| `data-msg-min`, `data-msg-max` | The number or date is below `min` or above `max`. |
| `data-msg-step` | The number does not fit `step`. |

### A message goes where you say, or right after the field

` ` marks the place for the field `email`. `_form` is the place for a message about the whole form. Without such an element, the browser creates one with the class `rual-web-error` and `role="alert"`, right after the field or its ` `. For `_form`, it goes at the top of the form. A field with a message gets `aria-invalid="true"`, and an empty place takes no room.

## The action checks what only the server can know

Is this email already registered? Is this product still in stock? Check it in the action and answer with [`web_errors`](https://docs.rual.nl/block-types/web/web_errors).

| Pin | Is |
| --- | --- |
| `errors` | An object of field name to message. Empty messages are dropped, and an empty object clears earlier messages. |
| `message` | A message for the whole form, shown in `[data-error-for="_form"]`. |
| `target` | A CSS selector for the form, when it is not the one that sent the action. |

```
flow subscribe = web_action with {event: "subscribe", scopes: ["*public"]} {
  let {email} = subscribe.form
  found = function_search_single_result@subscribers(query: query_bool_filter(term{email: email}))
  guard not found.has_hits else {
    web_errors(errors: {email: "This address is already subscribed."})
  }
  create@subscribers(mutations{email: email})
  web_replace(target = "form[data-action=subscribe]", html: "<p>Thanks for subscribing.</p>")
}
```

Each answer clears the form's earlier messages first, so a second attempt never shows an old one.

> **The browser checks are a convenience, not a gate** A visitor can switch them off, and a script can post anything. If a value matters, check it again in the action, or in a [`httpconnection_fieldvalidation`](https://docs.rual.nl/block-types/http%20connection/httpconnection_fieldvalidation) block.

## What arrives on the form pin

| The form has | The `form` pin holds |
| --- | --- |
| One field `email` | `email` as a text. Every value of a form post is a text, numbers too. |
| Two or more fields with the same name, or a multiple select with several chosen | That name as a list. |
| A field named `tags[]` | `tags` as a list, also with one value. Use this when one value must not turn into a plain text. |
| A checkbox that is not checked | Nothing for that name, as in any HTML form. |
| A `data-param-size="xl"` on the form or button | `size` with the value `xl`. It wins over a field of the same name. Attribute names are lower case in HTML, so the field name is too. |
| A submit button with a `name` | That name and the button's value, as in any HTML form. |
| A file input | Nothing: see the next section. |

`_action` and `_key` belong to the request and are taken out. `_key` arrives on the action's `key` pin. To put an error on a `name[]` field, use the name as it stands in the markup: `tags[]`.

## Bodies are small, and files go elsewhere

- A body may be at most **1 MiB**. A larger one is answered with `413`.

- An action takes urlencoded fields, as every form sends, or a JSON object from a script of your own. In JSON, `_action` and `_key` are keys of the object.

- **Multipart is refused with `415`.** The script in the page leaves file inputs out of what it sends. Upload files through the [upload endpoints](https://docs.rual.nl/tutorials/file-upload-processing), then pass the result to the action.

## A button can bring its own action

- A submit button with its own `data-action` runs that action with the form's fields, after the browser checks. Two submit buttons can do two different things.

- A `type="button"` with `data-action` inside a form sends the form's fields **without** the checks, which suits a "Save draft".

- `data-loading-text` changes the button's text while the action runs, and the button is disabled meanwhile.

- `data-msg-failed` on the form is the text shown in `[data-error-for="_form"]` if the request itself fails, such as when the network drops.

## Forms keep working without JavaScript

A plain form with a hidden `_action` field posts the same way. Write it so it works both ways:

```
<form method="post" data-action="subscribe">
  <input type="hidden" name="_action" value="subscribe">
  <label>Email <input name="email" type="email" required></label>
  <button type="submit">Subscribe</button>
</form>
```

The field must be in the body: an `_action` in the query string is ignored. For a row, add a hidden `_key`. Without JavaScript, the update blocks have no browser script to apply them, so the answer depends on what the flow did:

| The flow ran | The visitor gets |
| --- | --- |
| A [`web_render`](https://docs.rual.nl/block-types/web/web_render) | That page. |
| A [`web_redirect`](https://docs.rual.nl/block-types/web/web_redirect) | A `303` to its URL. |
| Anything else | A `303` back to the page. |

The `303` means a reload does not send the form again. [`web_errors`](https://docs.rual.nl/block-types/web/web_errors) shows nothing here, as there is no script to place the messages. If a form must show its errors without JavaScript, answer the failure with a [`web_render`](https://docs.rual.nl/block-types/web/web_render) of the same page that prints the message: that works with and without JavaScript. A `required` attribute still works either way, as the browser itself checks it.

## Next steps

- [Markup Reference](https://docs.rual.nl/web/markup): Every attribute in one place, with the hooks for your CSS.

- [Actions](https://docs.rual.nl/web/actions): The other blocks an action can answer with.

- [File Upload and Processing](https://docs.rual.nl/tutorials/file-upload-processing): Where files go, since a web action does not take them.

## Frequently asked

**Do RUAL web pages validate forms without a request to the server?**

Yes. A form with data-action checks required, type=email, pattern, minlength and min/max in the browser and shows the message next to the field without a request. The action only checks what the server knows and answers with web_errors; those messages disappear as soon as the visitor edits the field.

**Can a RUAL web action receive file uploads?**

No. Actions take form fields and JSON up to 1 MiB, and a multipart body is refused with a 415. Upload the file through the upload endpoints and pass the result to the action.
