Grav Plugin Forms — Form State, Validation and File Uploads
In this tutorial, you'll learn Grav plugin forms — form state management, validation rules, file upload handling, multi-step forms, and integrating custom forms with the Form plugin for data processing.
What You'll Learn
- The Form plugin architecture and configuration
- Creating forms in page frontmatter
- Form field types and validation rules
- File upload handling and security
- Multi-step forms with conditional logic
- Form data processing: email, database, webhooks
- Custom form templates and layout
Why It Matters
In WordPress, forms require plugins like Contact Form 7 or Gravity Forms. In Grav, forms are built into the Form plugin using YAML configuration. You define fields, validation, and actions (email, save, Webhook) in the page frontmatter. No PHP code needed for standard forms. For advanced forms, you extend the Form plugin with custom handlers. This means form creation is a configuration task, not a development task.
Real-World Use
A product documentation site needs a "Report a Bug" form that accepts: bug title, description, severity level, browser info (auto-detected), and a screenshot upload. The form saves submissions to a JSON file for the development team to review and sends an email notification. The entire form is configured in YAML — no backend code, no database, no third-party service.
Learning Path
flowchart LR
A["Plugin Events"] --> B["Plugin Forms
← You are here"]:::current
B --> C["Plugin Admin"]
C --> D["Plugin CLI"]
D --> E["Multilingual"]
E --> F["User Management"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Installing the Form Plugin
bin/gpm install form
The Form plugin provides the framework for creating, validating, and processing forms.
Creating a Basic Form
Forms are defined in page frontmatter. Create user/pages/06.contact/form.md:
---
title: Contact Us
form:
name: contact-form
fields:
name:
label: Name
placeholder: Enter your name
type: text
validate:
required: true
email:
label: Email
placeholder: Enter your email
type: email
validate:
required: true
pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
subject:
label: Subject
type: select
default: general
options:
general: General Inquiry
support: Technical Support
billing: Billing Question
validate:
required: true
message:
label: Message
placeholder: Your message here
type: textarea
validate:
required: true
min: 10
max: 5000
buttons:
submit:
type: submit
value: Send Message
reset:
type: reset
value: Clear
process:
- email:
subject: "New contact form submission"
from: "{{ form.value.email }}"
to: "support@dodatech.com"
- save:
file: "user://data/contacts/{{ form.value.name|slugize }}.json"
- message: "Thank you for contacting us!"
- reset: true
---
# Contact Us
Have a question or need help? Fill out the form below and we will get back to you.
Form Field Types
| Type | HTML | Use |
|---|---|---|
text |
<input type="text"> |
Single line text |
email |
<input type="email"> |
Email address |
number |
<input type="number"> |
Numeric input |
textarea |
<textarea> |
Multi-line text |
select |
<select> |
Dropdown list |
checkbox |
<input type="checkbox"> |
Single checkbox |
checkboxes |
Multiple checkboxes | Multiple selection |
radio |
Radio buttons | Single selection from options |
file |
<input type="file"> |
File upload |
hidden |
<input type="hidden"> |
Hidden field |
date |
<input type="date"> |
Date picker |
color |
<input type="color"> |
Color picker |
captcha |
reCAPTCHA | Spam protection |
Validation Rules
fields:
email:
type: email
validate:
required: true
pattern: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
message: "Please enter a valid email address"
age:
type: number
validate:
required: true
min: 18
max: 120
bio:
type: textarea
validate:
min: 10
max: 1000
message: "Bio must be between 10 and 1000 characters"
agree:
type: checkbox
validate:
required: true
File Uploads
fields:
screenshot:
type: file
label: Upload screenshot
multiple: false
destination: 'user://data/uploads/bugs'
accept:
- 'image/png'
- 'image/jpeg'
- 'image/gif'
- 'application/pdf'
validate:
required: false
max_file_size: 5242880 # 5MB
Upload Processing
process:
- uploads:
folder: 'user://data/uploads/bugs'
- email:
subject: "Bug report with attachment"
attachments: true
Multi-Step Forms
Define multiple form pages that submit progressively:
---
title: Registration
form:
name: multi-step-registration
steps:
account:
title: Account Details
fields:
username:
type: text
validate:
required: true
email:
type: email
validate:
required: true
profile:
title: Profile Information
fields:
fullname:
type: text
validate:
required: true
bio:
type: textarea
confirmation:
title: Confirm and Submit
fields:
agree_terms:
type: checkbox
label: "I agree to the terms and conditions"
validate:
required: true
process:
- email:
subject: "New registration"
to: "admin@example.com"
- message: "Registration complete!"
Form Data Processing Actions
process:
- email:
subject: "Form: {{ form.value.subject }}"
body: "{% include 'forms/data.html.twig' %}"
from: "{{ form.value.email }}"
to: "admin@example.com"
cc: "team@example.com"
bcc: "archive@example.com"
attachments: true
Save to File
process:
- save:
file: "user://data/form-submissions/{{ date('Y-m-d') }}.json"
Webhook
process:
- webhook:
url: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
method: POST
body: "New submission from {{ form.value.name }}"
Conditional Processing
process:
- if: "form.value.priority == 'high'"
then:
- email:
subject: "URGENT: High priority request"
to: "urgent@example.com"
else:
- email:
subject: "Standard request"
to: "normal@example.com"
Custom Form Template
Override the form template in your theme:
user/themes/mytheme/templates/forms/default-form.html.twig:
<form name="{{ form.name }}" method="post" action="{{ form.action }}" enctype="multipart/form-data">
{% for field in form.fields %}
<div class="form-group">
<label for="{{ field.name }}">{{ field.label }}</label>
{% if field.type == 'text' or field.type == 'email' %}
<input type="{{ field.type }}" name="{{ field.name }}"
value="{{ form.value(field.name) }}"
placeholder="{{ field.placeholder }}"
{% if field.validate.required %}required{% endif %} />
{% elseif field.type == 'textarea' %}
<textarea name="{{ field.name }}"
placeholder="{{ field.placeholder }}"
rows="5"
{% if field.validate.required %}required{% endif %}>{{ form.value(field.name) }}</textarea>
{% elseif field.type == 'select' %}
<select name="{{ field.name }}">
{% for key, option in field.options %}
<option value="{{ key }}" {% if form.value(field.name) == key %}selected{% endif %}>
{{ option }}
</option>
{% endfor %}
</select>
{% elseif field.type == 'file' %}
<input type="file" name="{{ field.name }}" accept="{{ field.accept|join(',') }}" />
{% endif %}
{% if field.validate.required %}
<span class="required-badge">Required</span>
{% endif %}
</div>
{% endfor %}
<div class="form-actions">
{% for button in form.buttons %}
<button type="{{ button.type }}" class="btn btn-{{ button.type }}">
{{ button.value }}
</button>
{% endfor %}
</div>
{{ nonce_field('form')|raw }}
</form>
Learning Path
flowchart LR
A["Plugin Events"] --> B["Plugin Forms
← You are here"]:::current
B --> C["Plugin Admin"]
C --> D["Plugin CLI"]
D --> E["Multilingual"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Common Mistakes
Not including the
nonce_fieldin the form: Grav's Form plugin requires a CSRF nonce field to prevent cross-site request forgery. Always include{{ nonce_field('form')|raw }}in form templates.Missing
destinationon file uploads: File uploads require adestinationpath. Without it, uploaded files are stored in a temporary location and lost after processing.Not setting max file size: Without
max_file_size, large file uploads can exhaust server memory or storage. Always set a reasonable maximum size.Forgetting
Processactions after form fields: Theprocesssection defines what happens after submission. Without it, the form collects data but never processes it.Using same form name on multiple pages: Each form on the site must have a unique
name. Duplicate form names cause data conflicts and unexpected behavior.
Practice Questions
What YAML section defines what happens after a form is submitted? Answer: The
processsection. It defines actions like send email, save to file, call webhook, or show a success message.How do you require a field to be filled in? Answer: Add
validate.required: trueto the field definition. Grav validates required fields before running the process actions.What is the purpose of the
nonce_fieldin a form? Answer: It generates a hidden CSRF token that prevents cross-site request forgery attacks. Grav validates this token when the form is submitted.How do you create a multi-step form with the Form plugin? Answer: Define
stepsunder theform:key. Each step is a named section with its own fields. Grav shows one step at a time with previous/next navigation.Challenge: Build a complete bug report form for a software project. Include fields for: bug title (required, text), severity (select: critical, major, minor, cosmetic), URL where bug was found (required, URL type), description (required, textarea, min 20 chars), steps to reproduce (textarea), browser and OS (autofilled via JavaScript hidden fields), screenshot upload (max 3 files, 5MB each, PNG/JPG only), and email for follow-up. Configure the form to save submissions to a JSON file, send email to the development team with attachments, and show a confirmation message. Add validation for all fields and test with sample submissions.
FAQ
Mini Project
Goal: Build a complete contact and feedback system with forms.
- Create a contact form with name, email, subject (select), message (textarea), and file upload
- Configure email processing to send to multiple recipients
- Configure file saving to store submissions as JSON
- Create a multi-step registration form with 3 steps
- Add reCAPTCHA to all forms for spam protection
- Create custom form templates for each form type
- Add conditional logic: show/hide fields based on previous selections
- Create a "Report a Problem" form with priority levels
- Add a feedback form at the bottom of documentation pages
- Test all forms: submission, validation, file uploads, email delivery, and error handling
What's Next
Now you can create complex forms. Next, learn to build admin features:
Continue to Lesson 27: Plugin Admin — Custom admin pages, admin widgets, and dashboard panels.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro