Published
5/30/2025
Categories
Software

Building a Multi-Step Form in October CMS 2 with jQuery, HTML5 Validation, and Centralized Saving

Building a Multi-Step Form in October CMS 2 with jQuery, HTML5 Validation, and Centralized Saving

To build a clean, maintainable user flow, we designed a multi-step form in October CMS 2 with these goals:

  • Keep form experience seamless via AJAX

  • Minimize duplication using a single controller handler

  • Use native HTML5 validation to prevent invalid data from reaching the server

In this article, I’ll walk through the setup using just two steps + a confirmation, and explain why we centralized the saving logic the way we did.

Why HTML5 Validation?

Using HTML5 validation means:

  • No custom JS required for basic required/format checks

  • Error display is automatic (reportValidity())

  • Invalid form data is blocked before any backend request

We call form.checkValidity() before running AJAX. This avoids wasted network calls and ensures real-time feedback for the user.

Remember you should still add validation on the server side including in the model creation.

Why a Single onSaveForm Handler?

When building multi-step forms, it’s tempting to write onSaveStep1, onSaveStep2, etc. But that leads to:

  • Repetition of shared logic 

  • Difficult-to-maintain state flow

  • Extra logic for managing public_key or tracking steps

Instead, we use one AJAX handler (onSaveForm) that dispatches to private methods like handleStepOne() or handleStepTwo() based on the posted form_step.

Step 1: Step One Form <div id="stepContainer">     <form id="multiStepForm">         <input type="hidden" name="form_id" value="form-multi-step">         <input type="hidden" name="form_step" value="1">         <input type="text" name="first_name" required>         <input type="email" name="email" required>         <button type="submit">Next</button>     </form> </div>

This is the first step of the multi-step form. It includes:

  • Two user-facing fields (first_name and email) with required HTML5 validation

  • Hidden inputs to identify the form (form_id) and the current step (form_step)

The form will be submitted via JavaScript, not a normal HTTP post jQuery Handler: $(document).on('submit', '#multiStepForm', function (e) {     e.preventDefault();     const form = this;     if (!form.checkValidity()) {         form.reportValidity();         return;     }     $.request('onSaveForm', {         data: $(form).serialize(),         update: {             '#stepContainer': '#stepContainer'         }     }); }); This jQuery handler:

  • Intercepts the form submission (e.preventDefault())

  • Checks if the form is valid using HTML5 validation (checkValidity())

  • If valid, it sends the form data via October CMS’s $.request() method to the onSaveForm AJAX handler

Updates the #stepContainer element with the response (i.e., the next step’s HTML) Centralized Controller Method public function onSaveForm() {     $formData = post();     $step = $formData['form_step'] ?? null;     if ($formData['form_id'] !== 'form-multi-step') {         throw new \ApplicationException("Invalid form submission.");     }     $record = $this->loadOrCreateRecord($formData['public_key'] ?? null);     switch ($step) {         case '1':             return $this->handleStepOne($formData, $record);         case '2':             return $this->handleStepTwo($formData, $record);         default:             throw new \ApplicationException("Unknown step.");     } }

This centralized handler:

  • Accepts form data from the AJAX request

  • Verifies that the form is recognized via form_id

  • Loads or creates the in-progress record using a public_key (if one exists)

  • Routes the request to a private step handler (handleStepOne, handleStepTwo) based on the current step

Returns a partial response with the next step’s content and state data.

Step Handler protected function handleStepOne($data, $record) {     $record->first_name = $data['first_name'] ?? '';     $record->email = $data['email'] ?? '';     $record->public_key = $record->public_key ?: \Str::uuid();     $record->current_step = 2;     $record->save();     return [         '#stepContainer' => $this->renderPartial('@_step2_form.htm', ['record' => $record]),         'public_key' => $record->public_key     ]; }

This method:

  • Saves the values from Step 1 to the current record

  • If the record doesn’t have a public_key, it generates one (to persist progress across steps)

  • Advances the step state by setting current_step = 2

  • Returns a partial (the Step 2 form) to replace the content inside #stepContainer

  • Also includes public_key in the response so it can be passed to the next form submission

Loading Steps Dynamically: public function onLoadStep() {     $step = post('step') ?? 1;     $publicKey = post('public_key');     $record = $publicKey ? SurveyRecord::where('public_key', $publicKey)->first() : null;     return [         '#stepContainer' => $this->renderPartial("@_step{$step}_form.htm", ['record' => $record])     ]; }

This method allows you to manually load a step without saving any new data. It:

  • Uses the posted step and public_key to look up the relevant form state

  • Renders the appropriate step partial (e.g., _step1_form.htm, _step2_form.htm)

Returns it to update the UI (same #stepContainer target)

By combining HTML5 validation, jQuery-powered AJAX, and a centralized save handler in October CMS 2, we’ve created a flexible and scalable structure for multi-step forms. This approach simplifies data handling, improves user experience, and makes it easier to maintain or extend the form over time.