Unpoly lets you build forms where one field re-renders other parts of the form when it changes. The new form state is rendered on the server, with your regular HTML templates and no additional JavaScript.
Common use cases for such forms are:
<select> value fetches options for another <select>.All of these are implemented with the [up-validate] attribute.
Tip
For light, client-side effects like toggling visibility, no server request is required. See Switching form state.
This is a form to purchase postage for international parcels:
The form has a select for the continent (Europe), a select for the country (Switzerland), a text field for the weight in grams (3500), and the price (23,00 €) shown as text, not as a field. A "Buy stamps" button submits the form.
The fields in this form have many dependencies between them:
We can implement this form with three [up-validate] attributes and no additional JavaScript:
<form method="post" action="/purchases">
<fieldset>
<label for="continent">Continent</label>
<select name="continent" id="continent" up-validate="#country">...</select> <!-- mark: up-validate="#country" -->
</fieldset>
<fieldset>
<label for="country">Country</label>
<select name="country" id="country" up-validate="#price">...</select> <!-- mark: up-validate="#price" -->
</fieldset>
<fieldset>
<label for="weight">Weight</label>
<input name="weight" id="weight" up-validate="#price"> kg <!-- mark: up-validate="#price" -->
</fieldset>
<fieldset>
<label for="price">Price</label>
<output id="price">23 €</output>
</fieldset>
<button>Buy stamps</button>
</form>
When a field is changed, Unpoly submits the form with an additional X-Up-Validate
HTTP header. Upon seeing this header, the server renders a new form state from the
form values in the request parameters.
When the server responds with the re-rendered form, Unpoly updates only
the target from the changed field's [up-validate] attribute.
For instance, when the continent is changed, only the country select is updated.
Everything else in the form keeps its state, including unsaved input in other fields.
The server needs to handle a validation request differently from a regular submission.
When it sees an X-Up-Validate header, it must not save the submitted data.
Instead it renders the form again from the request parameters, with dependent elements
reflecting the new values.
In a Ruby on Rails app this would look like this:
class PurchasesController < ApplicationController
def create
@purchase = Purchase.new(purchase_params)
if request.headers['X-Up-Validate'] # mark-line
@purchase.validate # mark-line
render 'form' # mark-line
elsif @purchase.save
redirect_to @purchase
else
render 'form', status: :unprocessable_entity
end
end
end
The controller validates @purchase without saving it, so the form can also show error messages
for the current values. The template for form renders the country options and the price from the @purchase object.
It does not need to know which field changed, since Unpoly extracts only the targeted fragment
from the response.
The examples use Ruby on Rails, but work similarly with any other backend.
See [up-validate] for the request and response in detail.
A field with [up-validate] may update multiple fragments
by separating their target selectors with a comma.
For instance, when the user changes the continent, the following would update the price preview in addition to the country select:
<select name="continent" up-validate="#country, #price"> <!-- mark: #country, #price -->
...
</select>
To update another fragment in addition to the field's form group, include
the group in the target list.
You can refer to the changed field as :origin:
<fieldset>
<select name="continent" up-validate="fieldset:has(:origin), #country, #price"> <!-- mark: fieldset:has(:origin) -->
...
</select>
</fieldset>
Users can change fields faster than your server responds. Unpoly keeps the form consistent with these rules:
Let's walk through a challenging scenario using the postage form example above:
| User input | Network | Form changes |
|---|---|---|
| Selects a continent | Request 1 for the country select | |
| Enters a parcel weight | waits for request 1 | |
| Changes the continent again | waits for request 1 | |
| Request 1 loads | Country select updated | |
| Request 2 for country select and price | ||
| Request 2 loads | Country select and price updated |
Once the last response is processed, all fields and the price preview show consistent values.
If you prefer to completely prevent user input during validation, give the form an
[up-watch-disable] attribute. This disables all form fields while validation requests are in flight:
<form method="post" action="/purchases" up-watch-disable> <!-- mark: up-watch-disable -->
...
</form>
You may also set [up-watch-disable] on individual fields, or on any element that contains fields.
See disabling fields while working for details.
To re-render a fragment of the form from your own code, pass it to up.validate():
up.validate('#price')
This submits the form with an X-Up-Validate header, like a change to an [up-validate] field would,
and updates the #price element with the response. You can combine [up-validate] attributes
and up.validate() calls within the same form. Their updates
are batched together and follow the same rules for
preventing race conditions.
By default, validation requests use the [method] and [action] attributes from the form element.
You can render content from another server endpoint by setting an
[up-validate-url] attribute on a form or field:
<form method="post" action="/order">
<input name="quantity" up-validate="#preview" up-validate-url="/preview-order"> <!-- mark: up-validate-url="/preview-order" -->
<div id="preview">
Order total: €190
</div>
</form>
Multiple validations to the same URL are batched together.
Tip Your coding agent can search these docs locally with the Unpoly agent skill