Learn Reactive server forms

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:

  • Picking a <select> value fetches options for another <select>.
  • Changing a field makes dependent form sections appear or disappear.
  • Typing into a field previews the effects of a submission, e.g. shows an order total.
  • Picking a value shows related data, like existing bookings for a selected customer.

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.

Declaring dependencies

This is a form to purchase postage for international parcels:

Mockup of a form to buy postage

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:

  • When the continent is changed, the country select must update to list countries from the selected continent.
  • When the country is changed, the price preview must update.
  • When the parcel weight is changed, the price preview must update.

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.

Rendering the form state on the server

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.

Multiple dependent fragments

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>

Preventing race conditions

Users can change fields faster than your server responds. Unpoly keeps the form consistent with these rules:

  • One request at a time. Each form has at most one validation request in flight. Later changes wait until it has loaded.
  • Batched updates. Changes that waited are sent together, in a single request for all their targets. When one target contains another, only the outer one is requested.
  • Submitting wins. When the user submits while a validation is loading, the validation is aborted. This happens when the submission updates the form or an element around it, like the main fragment.

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.

Disabling fields during validation

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.

Updating from JavaScript

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.

Rendering from other URLs

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.