Renders a new form state when a field changes, to show validation errors or update dependent fields.
When a form field with an [up-validate]
attribute is changed, the form is submitted to the server
which is expected to render a new form state from its current field values.
The form group around the changed field is updated with the server response.
This gives the user quick feedback whether their change is valid, without the need to scroll for error messages or to backtrack to fields completed earlier.
Note
[up-validate]
is a tool to implement highly dynamic forms that must update as the user is completing fields.
If you only need to validate forms after submission, you don't need[up-validate]
.
Let's look at a standard registration form that asks for an e-mail and password. The form is organized into form groups of labels, inputs and an optional error message:
<form action="/users">
<fieldset>
<label for="email">E-mail</label>
<input type="text" id="email" name="email">
</fieldset>
<fieldset>
<label for="password">Password</label>
<input type="password" id="password" name="password">
</fieldset>
<button type="submit">Register</button>
</form>
We have some data constraints that we want to validate as the user is filling in fields:
email
field, we want to validate that the e-mail address
is formatted correctly and is still available.password
field, we want to validate
the minimum password length.If validation fails we want to show validation errors as soon as the user blurs the field.
We're going to render validation errors using the following HTML:
<form action="/users">
<fieldset>
<label for="email" up-validate>E-mail</label>
<input type="text" id="email" name="email" value="foo@bar.com">
<div class="error">E-mail has already been taken!</div> <!-- mark-line -->
</fieldset>
<fieldset>
<label for="password" up-validate>Password</label>
<input type="password" id="password" name="password" value="secret">
<div class="error">Password is too short!</div> <!-- mark-line -->
</fieldset>
</form>
We can implement this by giving both fields an [up-validate]
attribute:
<form action="/users">
<fieldset>
<label for="email" up-validate>E-mail</label> <!-- mark-phrase "up-validate" -->
<input type="text" id="email" name="email">
</fieldset>
<fieldset>
<label for="password" up-validate>Password</label> <!-- mark-phrase "up-validate" -->
<input type="password" id="password" name="password">
</fieldset>
<button type="submit">Register</button>
</form>
Whenever a field with [up-validate]
changes, the form is submitted to its [action]
path
with an additional X-Up-Validate
HTTP header.
When the user changes the email
field in the registration form above,
the following request will be sent:
POST /users HTTP/1.1
X-Up-Validate: email
X-Up-Target: fieldset:has(#email)
Content-Type: application/x-www-form-urlencoded
email=foo%40bar.com&password=secret
Upon seeing an X-Up-Validate
header, the server is expected to validate (but not commit)
the form submission and render a new form state from the request parameters.
This requires a change in the backend code that handles the form's [action]
path.
Until now the backend only had to handle two cases:
A Ruby on Rails implementation would look like this:
class UsersController < ApplicationController
def create
# Instantiate model from request parameters
user_params = params.require(:user).permit(:email, :password)
@user = User.new(user_params)
if @user.save
# Form is submitted successfully
sign_in @user
else
# Submission failed
render 'form', status: :unprocessable_entity
end
end
end
To honor the validation protocol, our backend needs to handle a third case:
X-Up-Validate
header, render a new form state from request parametersIn our example backend above, this change could look like this:
class UsersController < ApplicationController
def create
# Instantiate model from request parameters
user_params = params.require(:user).permit(:email, :password)
@user = User.new(user_params)
if request.headers['X-Up-Validate'] # mark-line
@user.validate # mark-line
render 'form' # mark-line
elsif @user.save
# Form is submitted successfully
sign_in @user
else
# Submission failed
render 'form', status: :unprocessable_entity
end
end
end
Tip
If you're using Python with Django, you may find the
django-forms-dynamic
package useful to implement this pattern.
The server is free to respond with any HTTP status code, regardless of the validation result. Unpoly will always consider a validation request to be successful, even if the server responds with a non-200 status code.
Upon seeing an X-Up-Validate
header, the server now renders a new state form from request parameters,
showing eventual validation errors and updating dependent fields:
<form action="/users">
<fieldset>
<label for="email" up-validate>E-mail</label>
<input type="text" id="email" name="email" value="foo@bar.com">
<div class="error">E-mail has already been taken!</div> <!-- mark-line -->
</fieldset>
<fieldset>
<label for="password" up-validate>Password</label>
<input type="password" id="password" name="password" value="secret">
<div class="error">Password is too short!</div> <!-- mark-line -->
</fieldset>
</form>
[up-validate]
always submits the entire form with its current field values to the form's
[action]
path. Typically only a fragment of the form is updated with the response.
This minimizes the chance for loss of transient state like scroll positions, cursor selection
or user input while the request is in flight.
By default Unpoly will only update the closest form group
around the validating field. The example above,
after changing the email
field, only the <fieldset>
around the field will be updated.
If the form is not structured into groups, the entire form will be updated.
If you don't want to update the field's form group, you can set the [up-validate]
attribute to any target selector:
<input type="text" name="email" up-validate=".email-errors"> <!-- mark-phrase ".email-errors" -->
<div class="email-errors"></div>
You may also update multiple fragments by separating their target selectors with a comma:
<input type="text" name="email" up-validate=".email-errors, .base-errors"> <!-- mark-phrase ".email-errors, .base-errors" -->
The [up-validate]
attribute is a useful tool to partially update a form
when one fields depends on the value of another field.
See dependent fields for more details and examples.
By default [up-validate]
reacts to the change
event that is emitted
when the user is done editing and focuses the next field.
We can validate while the user is typing by setting an [up-watch-event="input"]
attribute:
<input name="email" up-validate up-watch-event="input" up-watch-delay="100" up-keep>
Note that we set some additional watch options to improve the experience:
[up-watch-delay]
attribute to delay validation until the user has stopped typing for 100 milliseconds.[up-keep]
attribute to preserve additional user input while the validation request is in flight.If you're using this pattern a lot, you may want to configure a macro for it.
Custom dynamic form implementations will often exhibit race conditions, e.g. when the user
is quickly changing fields while requests are still in flight.
Such issues are solved with [up-validate]
. The form will eventually show a consistent state,
regardless of how fast the user clicks or how slow the network is.
See preventing race conditions for more details.
You can set [up-validate]
on any element to validate all contained fields on change.
In the example above,
instead of setting [up-validate]
on each individual <input>
, we can also set it on the <form>
:
<form action="/users" up-validate> <!-- mark-phrase "up-validate" -->
<fieldset>
<label for="email" up-validate>E-mail</label>
<input type="text" id="email" name="email">
</fieldset>
<fieldset>
<label for="password" up-validate>Password</label>
<input type="password" id="password" name="password">
</fieldset>
<button type="submit">Register</button>
</form>
Multiple radio buttons with the same [name]
produce a single value for the form.
To watch radio buttons group, use the [up-validate]
attribute on an
element that contains all radio button elements with a given name:
<fieldset up-validate>
<input type="radio" name="format" value="html"> HTML format
<input type="radio" name="format" value="pdf"> PDF format
<input type="radio" name="format" value="txt"> Text format
</fieldset>
To update form fragments from your JavaScript, use the up.validate()
function.
You may combine [up-validate]
and up.validate()
within the same form. Their updates
will be batched together in order to
prevent race conditions.
The target selector to update with the server response.
Defaults the closest form group around the validating field.
The event types to observe.
The number of miliseconds to wait between an observed event and validating.
See debouncing callbacks.
Whether to disable fields while validation is running.
Whether to give navigation feedback while validating.