Liquid error (layouts/theme line 23): internal Skip to content
Log in

Form - Liquid Tag Reference

On this page

The {% form %} block tag generates an HTML form with built-in handling for submissions and validation. It automatically wraps content in a <form> element, includes CSRF protection, and provides a form drop that exposes fields, their attributes, and validation errors.

Syntax

```liquid

{% form “form-type” [, option: value, …] %} {% endform %} ```

Property Value
Tag Name form
Type Block tag
Source Hydrofoil (core)

Description

The form tag provides form rendering and submission handling in StoreConnect Liquid templates. It handles: - Automatic form wrapping with correct action, method, and CSRF protection - Field definitions and accessibility attributes through the form drop - Validation error tracking and display - Submission state preservation for re-displaying user input on validation failure

For comprehensive usage patterns, see Liquid forms guide.

Form drop structure

Inside a {% form %} block, you can access a form drop with the following attributes:

Form drop attributes

| Attribute | Type | Description | |———–|——|————-| | fields | Collection[FormField] | Every field the form type defines, looked up by name as form.fields["field_name"] | | errors | Collection[FormError] | Every validation error on the form, including those not tied to a single field | | path | String | The form’s submission URL path |

fields, errors and path are the whole drop. There is no shorthand attribute for a field.

Field access pattern

Every field is reached through fields, keyed by the field name:

```liquid

{{ form.fields[“email”].value }} ```

A bare form.username is not a field. FormDrop defines no such attribute, so the expression renders as an empty string and the rest of the page renders normally. The failure is silent in the page: nothing marks the spot, and undefined method username is reported only to the Console.

The key can be a variable, so a field name can be built at render time:

```liquid

{% assign field_name = “email” %} {{ form.fields[field_name].value }} ```

Field attributes

Each field in a form has the following attributes:

Attribute Type Description
name String The HTML name attribute for the field. Use this in <input> elements.
id String The HTML id attribute for the field (if assigned by the form)
value String The current value of the field. Preserved on re-render after validation failure.
original_value String The value of the field before the current request (useful for detecting changes)
required? Boolean Whether the field is required
errors Array Array of error messages specific to this field

A field carries no label. Write the label text yourself and wire it to the field with for and id.

Field rendering example

```liquid

<label for="{{ form.fields["email"].id }}">Email address</label> <input type="email" id="{{ form.fields["email"].id }}" name="{{ form.fields["email"].name }}" value="{{ form.fields["email"].value }}" {% if form.fields["email"].required? %}required{% endif %}> {% assign email = form.fields["email"] %} {% if email.errors != blank %} {{ email.errors.messages | join: "; " }} {% endif %}

```

Error handling

form.errors is a collection of FormError objects: one per field that failed, plus one carrying the field name base for errors that belong to the form as a whole. It is not a list of strings.

On a FormError Type Holds
field String The field the error belongs to, or base for the form itself
messages Collection[String] The messages, without the field name
full_messages Collection[String] The messages, each prefixed with the field name

Because each error holds a list, rendering them takes two loops:

```liquid

{% for error in form.errors %} {% for message in error.full_messages %} <li>{{ message }}</li> {% endfor %} {% endfor %} ```

Outputting {{ error }} directly renders the drop, not the message.

form.errors.size is the test for whether a submission failed. A field carries its own form.fields["email"].errors, but that is a single FormError rather than a collection, so test it with != blank rather than .size, which raises on it.

Field values survive a failed submission through form.fields["email"].value, so a re-rendered form keeps what the customer typed.

For the display patterns built on this, see Liquid forms guide.

Form parameters

When creating a form, you can pass options that control the form’s behavior:

Standard parameters

| Parameter | Type | Description | |———–|——|————-| | product | ProductDrop | The product for add-to-cart forms (preferred over product_id) | | product_id | String | The product ID for add-to-cart forms (use product instead when possible) | | id | String | HTML id attribute for the <form> element | | class | String | HTML class attribute for the <form> element | | data-* | String | Custom data attributes are passed through to the form element |

Form-specific parameters

Different form types accept different parameters. For example:

```liquid

{% form “remove-voucher”, voucher: current_voucher %} {% endform %}

{% form “add-to-cart”, product: current_product, id: “ProductForm” %} {% endform %}

{% form “checkout-customer-information”, class: “form-compact” %} {% endform %} ```

Registered form names

StoreConnect provides many built-in form types. See Liquid forms reference for the complete list, including:

  • Authentication: login, register, forgot-password, reset-password, accept-invitation
  • Cart & Checkout: add-to-cart, cart, checkout-customer-information, checkout-shipping-information, payment
  • Promotions: apply-voucher, remove-voucher, apply-promo-code
  • Account: account, account-missing-details
  • Custom: custom-form (for CMS-defined forms)
  • Other: privacy-settings, geolocation-select, booking-attendee-add, and more

Common form patterns

### Pattern 1: basic form with field rendering

```liquid

{% form “register” %}

<label for="{{ form.fields["firstname"].id }}">First name</label> <input type="text" id="{{ form.fields["firstname"].id }}" name="{{ form.fields["firstname"].name }}" value="{{ form.fields["firstname"].value }}" {% if form.fields["firstname"].required? %}required{% endif %}>
<label for="{{ form.fields["email"].id }}">Email address</label> <input type="email" id="{{ form.fields["email"].id }}" name="{{ form.fields["email"].name }}" value="{{ form.fields["email"].value }}" {% if form.fields["email"].required? %}required{% endif %}>

{% endform %} ```

Pattern 2: error display

```liquid

{% form “login” %} {% render “form_errors”, errors: form.errors %}

{% assign username = form.fields[“username”] %}

<input type="text" id="username" name="{{ form.fields["username"].name }}" value="{{ form.fields["username"].value }}" class="{% if username.errors != blank %}is-invalid{% endif %}"> {% if username.errors != blank %} {{ username.errors.messages | join: "; " }} {% endif %}

{% endform %} ```

Pattern 3: add to cart with product context

```liquid

{% form “add-to-cart”, product: current_product, id: “AddToCartForm” %}

<input type="number" id="quantity" name="{{ form.fields["quantity"].name }}" value="{{ form.fields["quantity"].value | default: 1 }}" min="1" max="100">

{% render “form_errors”, errors: form.errors %}

{% endform %} ```

Pattern 4: custom form from CMS

```liquid

{% if my_contact_form %} {% form “custom-form”, custom_form: my_contact_form, id: “ContactForm” %} {% for question in my_contact_form.questions %} <div class="form-group {% if question.is_required %}required{% endif %}">

    {% case question.question_type %}
    {% when "text" %}
      <input 
        type="text" 
        id="q_{{ question.id }}"
        name="{{ question.input_name }}" 
        value="{{ question.answer_value }}"
        {% if question.is_required %}required{% endif %}>
    
    {% when "text_area" %}
      <textarea 
        id="q_{{ question.id }}"
        name="{{ question.input_name }}"
        {% if question.is_required %}required{% endif %}>{{ question.answer_value }}</textarea>
    
    {% when "picklist" %}
      <select 
        id="q_{{ question.id }}"
        name="{{ question.input_name }}"
        {% if question.is_required %}required{% endif %}>
        <option value="">-- Select --</option>
        {% for option in question.picklist_values %}
          <option value="{{ option }}" {% if question.answer_value == option %}selected{% endif %}>
            {{ option }}
          </option>
        {% endfor %}
      </select>
    {% endcase %}
  </div>
{% endfor %}

{% render "form_errors", errors: form.errors %}

<button type="submit">Send</button>   {% endform %} {% endif %} ```

Was this article helpful?

Was this article helpful?