Form Options
All Total CMS form methods accept formSettings to control form behavior, appearance, and functionality.
Core Options
Section titled “Core Options”{{ cms.form.builder('products', { method : 'POST', # HTTP method (string, default: 'POST') class : 'custom-form', # CSS classes for form (string, default: '') buildError : 'Save failed', # Error message to display (string, default: '') helpStyle : 'popup', # Help text style : 'popup', etc. (string, default: '') save : 'Save Product', # Save button label (string, default: '') delete : 'Delete Product', # Delete button label (string, default: '') formType : 'object', # Form type : 'collection', 'schema', 'object' (string, default: '') schema : 'product', # Schema name to use (string, default: '') route : '/custom/submit', # Custom form submission route (string) label : 'Submit', # Button label for simple forms (string) refresh : true, # Refresh page after submission (bool, default: false) data : {}, # Pre-populated form data (array, default: [])}) }}Behavior Options
Section titled “Behavior Options”{{ cms.form.builder('products', { autosave: true, # Enable automatic saving (bool, default: false) helpOnHover: true, # Show help on hover (bool, default: false) helpOnFocus: false, # Show help on focus (bool, default: false) hideID: false, # Hide the ID field (bool, default: false) addOnly: true, # Security: Only allow creating new objects, never editing (bool, default: false) register: true, # Retarget the form at /admin/register/{collection} so the new user is also auto-logged-in (bool, default: false)}) }}Security: Add Only Forms
Section titled “Security: Add Only Forms”Use addOnly: true for forms on the public side of your website to prevent users from editing existing objects by manipulating URL parameters:
{# Public form - secure against ID manipulation #}{{ cms.form.builder('inquiries', { addOnly: true, newActions: [ { action: 'message', text: 'Thanks — we\'ll be in touch.' } ]}).addField('name').addField('email').addField('message', {field: 'textarea'}).build() }}Security Note: When addOnly is enabled:
- Any ID parameter in the URL or form is ignored
- The form will always create a new object, never update existing ones
- Protects against malicious users passing
?id=123to edit other users’ data
Public Registration Forms
Section titled “Public Registration Forms”For user-registration forms specifically, prefer register: true over plain addOnly. It retargets the form at the public registration endpoint (POST /admin/register/{collection}), which creates the user record AND auto-logs them in as part of the same request — so the visitor doesn’t have to immediately type the credentials they just submitted.
{# Public sign-up form - creates the user and signs them in #}{{ cms.form.builder('members', { register: true, newActions: [ { action: 'redirect', link: '/welcome' } ]}).addField('name').addField('email').addField('password', {field: 'password'}).build() }}Behaviour:
- Form posts to
/admin/register/{collection}instead of/api/collections/{collection} - Whether the new user is auto-logged in OR sent a verification email depends on the collection’s Require Email Verification setting (see below)
addOnlyis implied — the register endpoint only handles POST, no edit/update path exists- Image and file fields work the same way they do on any add-only form: uploads are deferred client-side until the parent save completes, then uploaded against the freshly-created user record
Required server-side opt-in:
The target collection must appear in config.auth.publicRegistration in config/tcms.php, otherwise the endpoint returns a 403 Forbidden:
return [ 'auth' => [ 'publicRegistration' => ['members'], ],];The allow-list is empty by default so the operator-only auth collection isn’t accidentally exposed to public signups. Opt in your member / customer collections explicitly.
Email Verification
Section titled “Email Verification”Per-collection, you can require new registrants to confirm their email before their account is activated. Enable Require Email Verification on the collection’s settings page (under Public Access).
When enabled:
-
The registration save creates the user with
active = false -
A verification email is sent containing a tokenized link (default 24h expiry)
-
The response JSON carries
meta.requiresVerification: true; the form builder hides the form and reveals any element markeddata-verification-message:{{ cms.form.builder('members', {register: true}).addField('email').addField('password', {field: 'password'}).build() }}<div data-verification-message hidden><h3>Check your email</h3><p>We've sent you a link to verify your account.</p></div> -
The form also dispatches a
cms:form:verification-requiredevent for custom JS (analytics, redirects, etc.) -
Clicking the link hits
GET /admin/verify-email/{token}, flipsactive = true, and redirects to the login page -
If the link expired, the user is redirected to
/admin/resend-verificationwhere they can request a fresh link
A “Resend verification email” link is auto-added below the login form whenever publicRegistration is non-empty.
Default behavior (when the toggle is off) is unchanged: the new user is authenticated and signed in before the response returns — no verification step.
Verification-related config (in config/tcms.php under auth):
| Key | Default | Purpose |
|---|---|---|
verificationTokenExpiry | 1440 (24h) | Minutes before a verification token expires |
verificationMailerId | '' | Optional custom mailer template ID; leave empty for the default Twig template at email/verify-email.twig |
Security caveats the operator owns:
- Anyone who reaches the registration endpoint can create a user record. Verification stops a bot from logging in, but it doesn’t stop them from filling the user table with junk records — gate the form with a CAPTCHA or rate limit even when verification is on.
- New users land in whatever default access group the auth collection’s schema assigns. If that group reaches gated content, every verified signup gains that access.
- Password validation (minimum length etc.) is the schema’s responsibility, not the endpoint’s.
- The verification flow assumes the auth schema has an
emailfield. If your custom schema doesn’t, the verification email cannot be sent — the account is still created (inactive) but stays in limbo unless an admin activates it manually.
Actions
Section titled “Actions”Configure form actions for different operations. Actions are arrays that support multiple sequential operations:
{{ cms.form.builder('products', { newActions: [ # Actions for new items (array, default: []) { action: 'redirect-object', link: '?id=' } ], editActions: [ # Actions for editing (array, default: []) { action: 'refresh' } ], deleteActions: [ # Actions for deletion (array, default: []) { action: 'redirect', link: '/admin/products' } ]}) }}Multiple Sequential Actions
Section titled “Multiple Sequential Actions”You can chain multiple actions that will execute sequentially. By default, if one action fails, subsequent actions won’t execute:
{{ cms.form.builder('products', { newActions: [ { action: 'message', text: 'Product created successfully!' }, { action: 'redirect-object', link: '?id=' } ]}) }}Continue on Failure
Section titled “Continue on Failure”Add continue: true to any action to continue executing subsequent actions even if that action fails. This is useful for optional actions like webhooks, analytics tracking, or notifications that shouldn’t block critical user-facing actions.
Behavior:
- Failed actions with
continue: trueare logged to the browser console with warnings - Form state remains “success” - users won’t see error messages
- Subsequent actions continue executing normally
- Perfect for fire-and-forget operations
Basic Example:
{{ cms.form.builder('products', { newActions: [ { action: 'webhook', link: 'https://api.example.com/notify', continue: true # If webhook fails, still redirect }, { action: 'redirect-object', link: '?id=' } ]}) }}Real-World Example - Analytics & Notifications:
{{ cms.form.builder('orders', { newActions: [ { action: 'webhook', link: 'https://analytics.example.com/track', continue: true # Don't block on analytics failure }, { action: 'webhook', link: 'https://slack.example.com/notify', continue: true # Don't block on Slack notification failure }, { action: 'webhook', link: 'https://email.example.com/send', continue: true # Don't block on email failure }, { action: 'redirect-object', link: '/orders/{id}' # Always redirect user to order page } ]}) }}Mixed Critical & Optional Actions:
{{ cms.form.builder('products', { newActions: [ { action: 'webhook', link: 'https://api.inventory.com/reserve' # No continue - inventory reservation is critical }, { action: 'webhook', link: 'https://api.analytics.com/track', continue: true # Analytics is optional }, { action: 'redirect-object', link: '/products/{id}' } ]}) }}Console Output:
When an action with continue: true fails, you’ll see:
Action execution failed: Error: Action webhook failed: 503 Service UnavailableAction failed but continuing due to continue: true {action: 'webhook', link: '...', continue: true}Available Action Types
Section titled “Available Action Types”-
redirect: Redirect to a specific URL
{action: 'redirect', link: '/admin/products'} -
redirect-object: Redirect to URL with object ID substitution
{action: 'redirect-object', link: '/admin/products/{id}'} -
refresh: Refresh the current page
{action: 'refresh'} -
message: Display a message (future: combined with other actions)
{action: 'message', text: 'Operation successful!'}
Auto-Applied CSS Classes
Section titled “Auto-Applied CSS Classes”The form system automatically applies CSS classes based on options:
.autosave- whenautosave: true.help-on-hover- whenhelpOnHover: true.help-on-focus- whenhelpOnFocus: true.help-{helpStyle}- whenhelpStyleis set (e.g.,.help-popup).edit-mode- when method is not ‘POST’.formgrid- when schema has formgrid configuration
Form State Classes
Section titled “Form State Classes”Forms automatically receive state classes during the save lifecycle that you can use for custom styling:
| Class | Description |
|---|---|
.unsaved | Form has unsaved changes |
.processing | Form is being saved |
.success | Form was saved successfully |
.error | An error occurred during save |
.actions-completed | All post-save actions (webhooks, mailers, redirects) completed successfully |
Note: The .actions-completed class is added in addition to .success, not as a replacement. This allows you to show different feedback for “saved” vs “all done”:
form.success { /* Data saved to server */ border-color: green;}
form.success.actions-completed { /* All actions completed (emails sent, webhooks fired, etc.) */ background-color: #e8f5e9;}Disabling the Status Banner
Section titled “Disabling the Status Banner”By default, Total CMS displays a full-screen status banner overlay when forms are processing, saved, or encounter errors. You can disable this banner for individual forms by adding the no-status-banner class:
{# Disable the global status banner for this form #}{{ cms.form.builder('products', { class: 'no-status-banner'}).addField('title').addField('price').build() }}When no-status-banner is set:
- The global overlay banner won’t appear for this form
- The form element still receives all state classes (
.processing,.success,.error,.unsaved,.actions-completed) - You have full control over styling the form’s feedback
Custom Styling Example:
/* Custom feedback for forms without the status banner */form.no-status-banner { position: relative; transition: border-color 0.3s ease;}
form.no-status-banner.unsaved { border-color: orange;}
form.no-status-banner.processing { border-color: blue; opacity: 0.7; pointer-events: none;}
form.no-status-banner.processing::after { content: "Saving..."; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.8); color: white; padding: 1rem 2rem; border-radius: 4px;}
form.no-status-banner.success { border-color: green;}
form.no-status-banner.success::after { content: "Saved!"; /* ... styling ... */}
form.no-status-banner.success.actions-completed::after { content: "All done!"; /* ... styling ... */}
form.no-status-banner.error { border-color: red;}Use Cases:
- Embedded forms where a full-screen overlay is disruptive
- Multiple forms on a page where you want individual feedback
- Custom-designed forms that need specific visual feedback
- Modal/dialog forms where the overlay conflicts with the modal