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, 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
<div data-error-for="email"></div> 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 <label>. 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.
| 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.
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,
_actionand_keyare 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, then pass the result to the action.
A button can bring its own action
- A submit button with its own
data-actionruns that action with the form's fields, after the browser checks. Two submit buttons can do two different things. - A
type="button"withdata-actioninside a form sends the form's fields without the checks, which suits a "Save draft". data-loading-textchanges the button's text while the action runs, and the button is disabled meanwhile.data-msg-failedon 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 | That page. |
A 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 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 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 Every attribute in one place, with the hooks for your CSS. Actions The other blocks an action can answer with. File Upload and 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.