Nimbu Developer Docs
Other

Forms

Build forms with validation, CSRF protection, and seamless integration with channels and customers

Nimbu provides a comprehensive form system with automatic CSRF protection, validation, file uploads, and integration with channels and customer accounts.

Basic Form Structure

{% form %}

Create forms for any content type:

{% form model %}
  {% input 'field_name' %}
  {% submit_tag 'Save' %}
{% endform %}

Supported Models:

  • channels.channel_name - Channel entry forms
  • customer - Customer account forms
  • order - Order/checkout forms
  • page - Page editable forms (with authentication)

Customer account forms use a name instead of a model, for example {% form login %}. The forms for two-step verification (two_factor, two_factor_start, two_factor_confirm, two_factor_backup_codes, two_factor_disable) are described in Customer Two-Step Verification.

Form Tags

{% input %}

Generate input fields with automatic attributes:

{% input 'name', label: 'Full Name', required: true %}

{% input 'email', as: 'email', placeholder: 'your@email.com' %}

{% input 'phone', as: 'tel', pattern: '[0-9]{10}' %}

Input Types (as parameter):

  • text (default)
  • email
  • tel
  • url
  • number
  • date
  • time
  • datetime-local
  • password
  • hidden

{% text_area %}

Multi-line text input:

{% text_area 'message', rows: 5, label: 'Your Message' %}

{% text_area 'bio', rows: 10, placeholder: 'Tell us about yourself...' %}

{% select %}

Dropdown selection:

{% select 'category', options: 'Option 1,Option 2,Option 3', label: 'Category' %}

{% select 'country', options: countries, label: 'Country' %}

{% checkbox %}

Single checkbox:

{% checkbox 'subscribe', label: 'Subscribe to newsletter' %}

{% checkbox 'terms', label: 'I agree to the terms', required: true %}

{% file_field %}

File upload:

{% file_field 'photo', label: 'Upload Photo', accept: 'image/*' %}

{% file_field 'document', label: 'Attach File', accept: '.pdf,.doc,.docx' %}

File inputs for custom fields

{% input 'field', as: 'file' %} and {% inputs_for_fields %} render file inputs for the file fields of a channel, customer or product. When the admin chose the field's file types (allowed_file_types), the input gets an accept attribute with those extensions:

{% form channels.aanvragen %}
  {% input 'offerte', as: 'file', label: 'Offerte' %}
{% endform %}
<input type="file" name="aanvragen[offerte]" id="aanvragen_offerte" class="input-file" accept=".pdf,.xls,.xlsx,.ods,.csv,.numbers">
  • A field without chosen file types renders exactly as before, without accept.
  • An accept: you pass in the tag wins over the field's.
  • accept only helps the browser's file picker. Nimbu checks the upload on the server either way.

The field hashes in channels.<slug>.custom_fields, submittable_fields and submittable_html_fields include the same data, so you can show it next to a custom input:

KeyPresent whenValue
max_file_sizethe admin set a maximum sizeBytes, e.g. 10485760
acceptthe admin chose file typesExtensions, e.g. .pdf,.xls,.xlsx,.ods,.csv,.numbers
{% for field in channels.aanvragen.custom_fields %}
  {% if field.name == 'offerte' and field.max_file_size %}
    <p class="hint">Max. {{ field.max_file_size | divided_by: 1048576 }} MB</p>
  {% endif %}
{% endfor %}

Both keys are missing for fields without these settings. The storefront defaults below never show up in them.

Upload limits for visitors

Files that visitors send through a theme form are checked against the field's settings and against the storefront defaults:

  • at most 50 MB per file, or the field's own maximum when that is lower
  • no SVG files and no "Other" files (css, js, xml, json, fonts, e-mail files, geo files, Flash) unless the admin allows those categories on the field

A file that breaks a limit gives a field error, which {% error_messages_for %} and the inline errors show like any other validation error, for example is too large (max. 10 MB) or file type .svg is not allowed (allowed: Images, PDF). The categories and how admins set them are described in Upload limits on file and gallery fields.

What changed for existing themes

Fields that nobody configured used to take any allowed file from visitors, with no size cap of their own. Now a form that lets visitors upload SVG, JavaScript, fonts or other files from the "Other" category, or files over 50 MB, gets a field error for those files. To accept them again, the admin allows the category on the field (Allowed File Types in the field editor). The 50 MB cap for visitors can't be raised. Theme markup doesn't change: inputs only get accept once an admin picks file types.

{% submit_tag %}

Submit button:

{% submit_tag 'Send Message' %}

{% submit_tag 'Save', class: 'btn btn-primary btn-lg' %}

Validation and Errors

{% error_messages_for %}

Display validation errors:

{% form channels.contact %}
  {% error_messages_for form_model %}

  {% input 'name', required: true %}
  {% input 'email', as: 'email', required: true %}
  {% text_area 'message', required: true %}

  {% submit_tag 'Send' %}
{% endform %}

Custom Error Styling:

{% error_messages_for form_model, class: 'alert alert-danger' %}

Practical Examples

Contact Form

<div class="contact-page">
  <h2>Get in Touch</h2>

  {% form channels.contact, class: 'contact-form' %}
    {% error_messages_for form_model %}

    <div class="form-row">
      <div class="col-md-6">
        {% input 'first_name', label: 'First Name', required: true %}
      </div>
      <div class="col-md-6">
        {% input 'last_name', label: 'Last Name', required: true %}
      </div>
    </div>

    <div class="form-group">
      {% input 'email', as: 'email', label: 'Email Address', required: true %}
    </div>

    <div class="form-group">
      {% input 'phone', as: 'tel', label: 'Phone Number' %}
    </div>

    <div class="form-group">
      {% select 'subject', options: 'General Inquiry,Support,Sales,Partnership', label: 'Subject', required: true %}
    </div>

    <div class="form-group">
      {% text_area 'message', rows: 6, label: 'Message', required: true %}
    </div>

    <div class="form-group">
      {% checkbox 'subscribe', label: 'Subscribe to our newsletter' %}
    </div>

    {% submit_tag 'Send Message', class: 'btn btn-primary' %}
  {% endform %}
</div>

Job Application Form

<div class="job-application">
  <h2>Apply for {{ job.title }}</h2>

  {% form channels.applications, class: 'application-form' %}
    {% error_messages_for form_model %}

    <input type="hidden" name="job_id" value="{{ job.id }}">

    <h3>Personal Information</h3>

    <div class="form-row">
      <div class="col-md-6">
        {% input 'first_name', label: 'First Name', required: true %}
      </div>
      <div class="col-md-6">
        {% input 'last_name', label: 'Last Name', required: true %}
      </div>
    </div>

    <div class="form-group">
      {% input 'email', as: 'email', label: 'Email', required: true %}
    </div>

    <div class="form-group">
      {% input 'phone', as: 'tel', label: 'Phone Number', required: true %}
    </div>

    <div class="form-group">
      {% input 'linkedin', as: 'url', label: 'LinkedIn Profile URL' %}
    </div>

    <h3>Application Details</h3>

    <div class="form-group">
      {% file_field 'resume', label: 'Resume (PDF)', accept: '.pdf', required: true %}
    </div>

    <div class="form-group">
      {% file_field 'cover_letter', label: 'Cover Letter (PDF)', accept: '.pdf' %}
    </div>

    <div class="form-group">
      {% text_area 'why_you', rows: 6, label: 'Why do you want to work with us?', required: true %}
    </div>

    <div class="form-group">
      {% input 'start_date', as: 'date', label: 'Available Start Date' %}
    </div>

    <div class="form-group">
      {% checkbox 'gdpr_consent', label: 'I consent to the processing of my personal data', required: true %}
    </div>

    {% submit_tag 'Submit Application', class: 'btn btn-lg btn-primary' %}
  {% endform %}
</div>

Customer Registration

<div class="registration-page">
  <h2>Create Account</h2>

  {% form customer, class: 'registration-form' %}
    {% error_messages_for form_model %}

    <div class="form-group">
      {% input 'email', as: 'email', label: 'Email Address', required: true %}
    </div>

    <div class="form-group">
      {% input 'password', as: 'password', label: 'Password', required: true %}
    </div>

    <div class="form-group">
      {% input 'password_confirmation', as: 'password', label: 'Confirm Password', required: true %}
    </div>

    <div class="form-row">
      <div class="col-md-6">
        {% input 'first_name', label: 'First Name', required: true %}
      </div>
      <div class="col-md-6">
        {% input 'last_name', label: 'Last Name', required: true %}
      </div>
    </div>

    <div class="form-group">
      {% checkbox 'newsletter', label: 'Send me news and special offers' %}
    </div>

    <div class="form-group">
      {% checkbox 'terms', label: 'I agree to the Terms and Conditions', required: true %}
    </div>

    {% submit_tag 'Create Account', class: 'btn btn-primary btn-block' %}
  {% endform %}

  <p class="login-link">
    Already have an account? <a href="/login">Sign in</a>
  </p>
</div>

Newsletter Signup

<section class="newsletter-signup">
  <div class="container">
    <h3>Stay Updated</h3>
    <p>Get the latest news and exclusive offers.</p>

    {% form channels.newsletter, class: 'newsletter-form' %}
      {% error_messages_for form_model %}

      <div class="form-inline">
        {% input 'email', as: 'email', placeholder: 'your@email.com', required: true %}
        {% submit_tag 'Subscribe', class: 'btn btn-primary' %}
      </div>
    {% endform %}
  </div>
</section>

Event Registration with Multiple Attendees

<div class="event-registration">
  <h2>Register for {{ event.title }}</h2>

  {% form channels.registrations, class: 'registration-form' %}
    {% error_messages_for form_model %}

    <input type="hidden" name="event_id" value="{{ event.id }}">

    <h3>Primary Attendee</h3>

    <div class="form-row">
      <div class="col-md-6">
        {% input 'first_name', label: 'First Name', required: true %}
      </div>
      <div class="col-md-6">
        {% input 'last_name', label: 'Last Name', required: true %}
      </div>
    </div>

    <div class="form-group">
      {% input 'email', as: 'email', label: 'Email', required: true %}
    </div>

    <div class="form-group">
      {% input 'company', label: 'Company Name' %}
    </div>

    <h3>Ticket Information</h3>

    <div class="form-group">
      {% select 'ticket_type', options: 'Standard,VIP,Student', label: 'Ticket Type', required: true %}
    </div>

    <div class="form-group">
      {% input 'quantity', as: 'number', label: 'Number of Tickets', value: '1', min: '1', max: '10', required: true %}
    </div>

    <h3>Dietary Requirements</h3>

    <div class="form-group">
      {% checkbox 'vegetarian', label: 'Vegetarian' %}
      {% checkbox 'vegan', label: 'Vegan' %}
      {% checkbox 'gluten_free', label: 'Gluten-free' %}
    </div>

    <div class="form-group">
      {% text_area 'special_requests', rows: 3, label: 'Special Requests or Allergies' %}
    </div>

    {% submit_tag 'Complete Registration', class: 'btn btn-lg btn-success' %}
  {% endform %}
</div>

Product Review Form

<div class="review-form">
  <h3>Write a Review</h3>

  {% form channels.reviews, class: 'product-review-form' %}
    {% error_messages_for form_model %}

    <input type="hidden" name="product_id" value="{{ product.id }}">

    <div class="form-group">
      <label>Rating *</label>
      <div class="rating-input">
        {% for i in (1..5) %}
          <input type="radio" name="rating" id="rating-{{ i }}" value="{{ i }}" required>
          <label for="rating-{{ i }}">ā˜…</label>
        {% endfor %}
      </div>
    </div>

    <div class="form-group">
      {% input 'title', label: 'Review Title', required: true %}
    </div>

    <div class="form-group">
      {% text_area 'comment', rows: 5, label: 'Your Review', required: true %}
    </div>

    <div class="form-group">
      {% input 'reviewer_name', label: 'Your Name', required: true %}
    </div>

    <div class="form-group">
      {% input 'reviewer_email', as: 'email', label: 'Email (not published)', required: true %}
    </div>

    <div class="form-group">
      {% checkbox 'verified_purchase', label: 'I purchased this product' %}
    </div>

    {% submit_tag 'Submit Review', class: 'btn btn-primary' %}
  {% endform %}
</div>

Advanced Form Patterns

Conditional Fields

{% form channels.bookings %}
  {% select 'booking_type', options: 'Individual,Group', label: 'Booking Type' %}

  <div class="conditional-fields" data-show-if="booking_type:Group">
    {% input 'group_name', label: 'Group Name' %}
    {% input 'group_size', as: 'number', label: 'Number of People' %}
  </div>

  {% submit_tag 'Book Now' %}
{% endform %}

<script>
document.querySelector('[name="booking_type"]').addEventListener('change', function(e) {
  const groupFields = document.querySelector('[data-show-if="booking_type:Group"]');
  groupFields.style.display = e.target.value === 'Group' ? 'block' : 'none';
});
</script>

AJAX Form Submission

{% form channels.contact, class: 'ajax-form', id: 'contact-form' %}
  {% input 'name', required: true %}
  {% input 'email', as: 'email', required: true %}
  {% text_area 'message', required: true %}

  <div class="form-status"></div>

  {% submit_tag 'Send Message' %}
{% endform %}

<script>
document.getElementById('contact-form').addEventListener('submit', async function(e) {
  e.preventDefault();

  const formData = new FormData(this);
  const statusDiv = this.querySelector('.form-status');

  try {
    const response = await fetch(this.action, {
      method: 'POST',
      body: formData,
      headers: {
        'Accept': 'application/json'
      }
    });

    if (response.ok) {
      statusDiv.innerHTML = '<div class="alert alert-success">Message sent successfully!</div>';
      this.reset();
    } else {
      const errors = await response.json();
      statusDiv.innerHTML = `<div class="alert alert-danger">${errors.message}</div>`;
    }
  } catch (error) {
    statusDiv.innerHTML = '<div class="alert alert-danger">An error occurred. Please try again.</div>';
  }
});
</script>

Best Practices

1. CSRF Protection

All Nimbu forms automatically include CSRF tokens. Never disable this:

<!-- āœ… Good: Automatic CSRF protection -->
{% form model %}
  <!-- form fields -->
{% endform %}

2. Required Field Validation

<!-- āœ… Good: Use required attribute -->
{% input 'email', as: 'email', required: true %}

<!-- āœ… Good: Client + server validation -->
{% input 'age', as: 'number', min: '18', required: true %}

3. Accessible Labels

<!-- āœ… Good: Descriptive labels -->
{% input 'phone', as: 'tel', label: 'Phone Number (optional)' %}

<!-- āŒ Bad: No label -->
<input type="text" name="phone">

4. Error Handling

<!-- āœ… Good: Show errors clearly -->
{% form model %}
  {% error_messages_for form_model, class: 'alert alert-danger' %}
  <!-- form fields -->
{% endform %}

Next Steps

Build powerful, accessible forms with Nimbu!

On this page