|
| 1 | +# RestFormCtrl |
| 2 | + |
| 3 | +`RestFormCtrl` is a controller that modernizes a traditional HTML form by replacing standard submit behavior with an asynchronous JSON request. It offers client-side validation, inline validation messages, and integrates with the `webexpress` event system. Additionally, the form can be initially populated with data from an API. |
| 4 | + |
| 5 | +Decoupling form presentation from data processing provides significant advantages over classic server-side form submits: |
| 6 | + |
| 7 | +- **Single-Page Application (SPA) Feeling:** The page does not need to reload upon submission. The user retains context, scroll position, and focus. |
| 8 | +- **Structured Data:** Data is sent as a typed JSON object. This facilitates backend processing compared to `x-www-form-urlencoded` and enables complex, nested structures. |
| 9 | +- **Precise Feedback:** Validation errors from the server can be mapped exactly to the affected fields without re-rendering the form. |
| 10 | +- **Status Feedback:** Events allow easy visualization of loading states (e.g., "Saving...") while the form is locked to prevent duplicate entries. |
| 11 | + |
| 12 | +## Data Population Strategies |
| 13 | + |
| 14 | +The controller supports two ways to load data into the form. These can also be combined (hybrid approach). |
| 15 | + |
| 16 | +1. **Server-Side Rendering (Recommended for Initial Load):** The most performant method is to deliver the HTML form already populated from the server. Here, the template engine sets the `value` attributes of inputs and the `checked` status of checkboxes directly in the HTML. |
| 17 | +2. **Population via REST API (Client-Side):** Using the `data-api` attribute, the controller can be instructed to execute a GET request after initialization to retrieve the data. |
| 18 | + |
| 19 | +## Declarative Configuration |
| 20 | + |
| 21 | +The behavior of REST-based forms is defined entirely through declarative configuration using `data-*` attributes on the `<form>` element. By attaching the class `wx-webapp-restform`, the controller automatically interprets these attributes to determine how the form should load, validate, submit, and handle errors. This approach minimizes the need for custom JavaScript and ensures a consistent, predictable workflow across different forms. The following table outlines the available attributes and their specific roles in controlling form functionality. |
| 22 | + |
| 23 | +| Attribute | Description |
| 24 | +|---------------------------|-------------------------------------------------------------------------------------- |
| 25 | +| `data-api` | URL of the endpoint to which data is sent. If not set, `action` is used. |
| 26 | +| `data-method` | HTTP method for submission. Overrides the form's `method` attribute. |
| 27 | +| `data-json` | Determines whether data is sent as JSON (`true`) or `x-www-form-urlencoded` (`false`). |
| 28 | +| `data-validate-on-submit` | Enables client-side validation before sending. |
| 29 | +| `data-show-inline-errors` | Displays error messages directly below the affected fields. |
| 30 | +| `data-mode` | Force `new`, `edit` or `delete` |
| 31 | +| `data-id` | Primary key for Edit/Delete; appended as query parameter. |
| 32 | + |
| 33 | +## Functionality and Behavior |
| 34 | + |
| 35 | +The form controller manages the complete lifecycle of user interactions, from initialization to error handling. Its behavior ensures that forms are consistently prepared, validated, and submitted in a reliable way, while providing meaningful feedback to users. The following points outline the core aspects of how the controller operates, including setup, validation, submission flow, and error management. |
| 36 | + |
| 37 | +- **Initialization & Hydration** |
| 38 | + - Upon loading, a container for global error messages (`.restform-error-container`) is created if it does not yet exist. |
| 39 | + - The controller retrieves the data and automatically populates the form fields. |
| 40 | + |
| 41 | +- **Validation** |
| 42 | + - The controller uses the native HTML5 validation API (`required`, `pattern`, `min`/`max`, `type="email"`, etc.). |
| 43 | + - In case of errors, submission is prevented, the first invalid field is focused, and an error message is displayed. |
| 44 | + - Specific validations for email patterns are additionally checked to compensate for browser inconsistencies. |
| 45 | + |
| 46 | +- **Submission** |
| 47 | + - The browser's standard submit is prevented. |
| 48 | + - All form fields (except `type="file"`) are serialized into a JSON object. |
| 49 | + - Checkbox groups and multi-selects are correctly processed as arrays or booleans. |
| 50 | + - During the submission process, the form is locked (`disabled`) and receives the CSS class `.restform-submitting`. |
| 51 | + |
| 52 | +- **Error Handling** |
| 53 | + - **Client-side:** Inline error messages are displayed directly at the field (`aria-invalid="true"`). |
| 54 | + - **Server-side:** If the server responds with status `400` and a JSON object `{ errors: { fieldName: "Message" } }`, these errors are assigned to the corresponding fields. |
| 55 | + - **Global:** Other errors (network, Server 500) are displayed in a global alert box above the form. |
| 56 | + |
| 57 | +## Programmatic Control |
| 58 | + |
| 59 | +The instance can be retrieved to manually control the form or register external hooks. |
| 60 | + |
| 61 | +### Accessing an Automatically Created Instance |
| 62 | + |
| 63 | +For forms declared in HTML, the associated RestFormCtrl instance is retrieved via the `getInstanceByElement(element)` method of the central `webexpress.webui.Controller`. |
| 64 | + |
| 65 | +```javascript |
| 66 | +// find the host form element in the DOM |
| 67 | +const formEl = document.getElementById('my-form'); |
| 68 | + |
| 69 | +// retrieve the RestFormCtrl instance associated with the element |
| 70 | +const restForm = webexpress.webui.Controller.getInstanceByElement(formEl); |
| 71 | + |
| 72 | +// change options and register hooks programmatically |
| 73 | +if (restForm) { |
| 74 | + // set onSuccess hook to run after a successful submit |
| 75 | + // note: hooks run inside try/catch in the controller; avoid throwing here |
| 76 | + restForm.options.onSuccess = function(json, response) { |
| 77 | + // navigate or show a notification |
| 78 | + window.location.href = '/success'; |
| 79 | + }; |
| 80 | + |
| 81 | + // add a beforeSend hook to mutate or cancel the payload |
| 82 | + restForm.options.beforeSend = function(payload, element) { |
| 83 | + // attach a timestamp and return the modified payload |
| 84 | + payload.ts = Date.now(); |
| 85 | + return payload; // return false to cancel submit |
| 86 | + }; |
| 87 | + |
| 88 | + // trigger validation manually |
| 89 | + const valid = restForm.validate(); |
| 90 | + if (valid) { |
| 91 | + // submit programmatically |
| 92 | + restForm.submit(); |
| 93 | + } |
| 94 | + |
| 95 | + // clear visible errors |
| 96 | + restForm.clear(); |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +### Manual Instantiation |
| 101 | + |
| 102 | +A RestFormCtrl can also be created entirely programmatically and attached to a host element. This is useful for dynamic UI scenarios. |
| 103 | + |
| 104 | +```javascript |
| 105 | +// find or create a container element for the form |
| 106 | +const container = document.getElementById('dynamic-form-container'); |
| 107 | + |
| 108 | +// create a form element and append it to the container |
| 109 | +const form = document.createElement('form'); |
| 110 | +form.className = 'wx-webapp-restform'; |
| 111 | +form.setAttribute('data-api', '/api/items'); |
| 112 | +container.appendChild(form); |
| 113 | + |
| 114 | +// instantiate RestFormCtrl manually for the newly created form |
| 115 | +const dynamicRestForm = new webexpress.webapp.RestFormCtrl(form); |
| 116 | + |
| 117 | +// set hooks and call methods as needed |
| 118 | +if (dynamicRestForm) { |
| 119 | + dynamicRestForm.options.onSuccess = function(json, response) { |
| 120 | + // show a confirmation or update UI |
| 121 | + console.log('saved', json); |
| 122 | + }; |
| 123 | + |
| 124 | + // optionally prefill and then submit |
| 125 | + dynamicRestForm.load().then(function() { |
| 126 | + // submit after load completes if desired |
| 127 | + dynamicRestForm.submit(); |
| 128 | + }).catch(function(e) { |
| 129 | + // handle load errors |
| 130 | + console.error(e); |
| 131 | + }); |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +## Accessibility |
| 136 | + |
| 137 | +To ensure that forms are usable and inclusive for all users, accessibility features are integrated directly into the interaction flow. These measures help screen readers, assistive technologies, and keyboard navigation provide clear feedback and maintain a consistent user experience. The following points outline how error handling, status management, and focus control are implemented to meet accessibility standards. |
| 138 | + |
| 139 | +- **Error Messages:** |
| 140 | + - Invalid fields receive the attribute `aria-invalid="true"`. |
| 141 | + - The error message is linked to the input field via `aria-describedby`. |
| 142 | +- **Status:** |
| 143 | + - During loading or submission, all interactive elements are disabled to prevent inconsistent states. |
| 144 | +- **Focus Management:** |
| 145 | + - In the event of validation errors, focus is automatically set to the first affected field. |
| 146 | + |
| 147 | +## Events |
| 148 | + |
| 149 | +The form controller communicates its internal lifecycle through a series of dispatched webexpress.webui.Event types. These events provide hooks for developers to monitor and react to key stages such as data requests, asynchronous operations, successful submissions, or error handling. Each event constant is triggered at a specific point in the process and carries a detailed payload that can be used for logging, UI updates, or custom logic. The following table summarizes the available events, their triggers, and the associated payload structure. |
| 150 | + |
| 151 | +| Event Constant | Trigger | Detail Payload |
| 152 | +|------------------------|-----------------------------------|------------------------------------ |
| 153 | +| `DATA_REQUESTED_EVENT` | Start of load/submit. | `{ type: "load"|"submit", url }` |
| 154 | +| `TASK_START_EVENT` | Async start. | `{ name: "loading"|"submitting" }` |
| 155 | +| `TASK_FINISH_EVENT` | Async end. | `{ name: "loading"|"submitting" }` |
| 156 | +| `DATA_ARRIVED_EVENT` | Data received. | `{ data, status, type: "load"|"submit" }` |
| 157 | +| `CHANGE_VALUE_EVENT` | Form populated (load). | `{ source: "load", data }` |
| 158 | +| `UPLOAD_SUCCESS_EVENT` | Submit succeeded (2xx). | `{ response, status, form }` |
| 159 | +| `UPLOAD_ERROR_EVENT` | Error (network, validation, 5xx). | `{ error|response, type: "validation"|undefined }` |
| 160 | + |
| 161 | +## Examples |
| 162 | + |
| 163 | +The following examples illustrate how to use REST-based forms within a web application. They show different scenarios, such as automatically loading and updating user profile data with the PUT method, or handling deletion requests with a static confirmation message. |
| 164 | + |
| 165 | +**Form with Automatic Loading and PUT Method:** |
| 166 | + |
| 167 | +This example demonstrates a form that automatically loads the current profile data and submits updates to the API using the PUT method. It is ideal for edit functions such as updating user information. In addition to validating the username, the user can also manage newsletter subscription preferences. Errors are displayed in a dedicated container, and changes are saved via a clearly marked button. |
| 168 | + |
| 169 | +```html |
| 170 | +<form id="profile-edit" |
| 171 | + class="wx-webapp-restform" |
| 172 | + data-api="/api/profile" |
| 173 | + data-method="PUT"> |
| 174 | + <div class="restform-error-container"></div> |
| 175 | + <div class="mb-3"> |
| 176 | + <label for="username">Username</label> |
| 177 | + <input type="text" name="username" class="form-control" required minlength="3"> |
| 178 | + </div> |
| 179 | + <div class="form-check"> |
| 180 | + <input class="form-check-input" type="checkbox" name="newsletter" id="newsletter"> |
| 181 | + <label class="form-check-label" for="newsletter">Subscribe to newsletter</label> |
| 182 | + </div> |
| 183 | + <button type="submit" class="btn btn-success">Save</button> |
| 184 | +</form> |
| 185 | +``` |
| 186 | + |
| 187 | +**Delete form without data load (static notice only, with `<confirm>`):** |
| 188 | + |
| 189 | +The second example illustrates a delete form that does not require loading data beforehand. Instead, a static confirmation message is shown through the <confirm> element once the action has been successfully completed. Before deletion, the user is prompted with a clear confirmation question and can proceed deliberately using the red Delete button. |
| 190 | + |
| 191 | +```html |
| 192 | +<form id="user-delete" |
| 193 | + class="wx-webapp-restform" |
| 194 | + data-api="/api/users" |
| 195 | + data-mode="delete" |
| 196 | + data-id="42"> |
| 197 | + <confirm> |
| 198 | + <div class="alert alert-success">User was successfully deleted.</div> |
| 199 | + </confirm> |
| 200 | + <p>Do you really want to delete this user?</p> |
| 201 | + <button type="submit" class="btn btn-danger">Delete</button> |
| 202 | +</form> |
| 203 | +``` |
0 commit comments