SmartForm
A declarative AJAX and native form engine built for the SmartComponents framework.
Wrap any combination of <smart-input>, <smart-search-input>,
and <smart-quill> fields inside <smart-form> and get
automatic value collection, client-side validation, AJAX submission, CSRF handling, error
display, toast feedback, loader overlay, table refresh, and success UI — with zero manual
JavaScript required.
<script src="smart-form.js"></script>
Load Order
SmartForm coordinates with the other SmartComponents files but never hard-requires them.
Every integration degrades gracefully — if smart-core.js isn't loaded,
toasts and loaders are simply skipped. Recommended order:
<!-- 1. Smart Core — toast · loader · modal (optional but recommended) --> <script src="smart-core.js"></script> <!-- 2. Input components (load whichever you use) --> <script type="module" src="input.js"></script> <script type="module" src="smart_search_input.js"></script> <script type="module" src="rich_text_input.js"></script> <!-- 3. SmartTable (optional — needed only for refresh-target) --> <script type="module" src="smart-table.js"></script> <!-- 4. SmartForm --> <script type="module" src="smart-form.js"></script> <!-- Global singletons — once in base.html --> <smart-toast position="bottom-right" max="5"></smart-toast> <smart-modal></smart-modal> <smart-loader type="overlay"></smart-loader>
AJAX Mode
Set api-url to activate AJAX mode. SmartForm intercepts the submit event,
collects all field values, sends a fetch() request, then handles the response
automatically — showing a success toast or mapping field errors back onto the form.
The browser never navigates away.
<smart-form api-url="/api/users/" method="POST" response-map='{"successPath":"status","messagePath":"message","errorsPath":"errors"}' > <smart-input type="text" name="username" label="Username" required></smart-input> <smart-input type="email" name="email" label="Email" required></smart-input> <button type="submit">Save</button> </smart-form>
Native Mode
Set mode="native" (or omit api-url) to let the browser submit
the form normally to the action URL. Add client-validate to still
run SmartComponent validation before the browser navigates.
<smart-form mode="native" action="/submit/" method="POST" client-validate > <!-- Django CSRF --> <smart-input type="text" name="title" label="Title" required></smart-input> <button type="submit">Submit</button> </smart-form>
Auto Mode (default)
When mode is omitted or set to "auto", SmartForm picks AJAX if
api-url is present, native otherwise. This is the default and covers most cases.
// mode="auto" — resolved at submit time // api-url present → AJAX mode (intercepts submit, uses fetch) // api-url absent → native mode (browser submits to action URL) <smart-form api-url="/api/save/"> <!-- auto → AJAX --> <smart-form action="/save/"> <!-- auto → native -->
Basic AJAX Form
The simplest SmartForm setup. Add api-url, drop in your
<smart-input> fields, and add a submit button.
SmartForm collects all field values automatically and POSTs them as JSON.
<smart-form api-url="/api/users/" method="POST" client-validate > <smart-input type="text" name="full_name" label="Full Name" required></smart-input> <smart-input type="email" name="email" label="Email Address" required></smart-input> <smart-input type="password" name="password" label="Password" required></smart-input> <smart-input type="select" name="role" label="Role" data-options='[ {"id":"admin","name":"Admin"}, {"id":"editor","name":"Editor"}, {"id":"viewer","name":"Viewer"} ]' required></smart-input> <button type="submit">Create Account</button> </smart-form>
Payload sent to the server (JSON body):
// Content-Type: application/json { "full_name": "Alice Smith", "email": "alice@example.com", "password": "••••••••", "role": "editor" }
Client Validation
Add client-validate to run all field validate() methods before
submitting. Each SmartComponent handles its own visual error state — invalid fields shake,
show red borders, and display their error message. Submission is blocked until all fields
pass. This works in both AJAX and native mode.
<!-- client-validate runs all field.validate() before submit --> <smart-form api-url="/api/articles/" client-validate > <smart-input type="text" name="title" label="Article Title" required></smart-input> <smart-input type="datepicker" name="publish_date" label="Publish Date" required></smart-input> <smart-input type="select" name="status" label="Status" data-options='[ {"id":"","name":"Select…"}, {"id":"draft","name":"Draft"}, {"id":"published","name":"Published"} ]' required></smart-input> <button type="submit">Save Article</button> </smart-form> // Each field's validate() is called in order. // If any return false, submission is blocked. // Fields handle their own shake + red-border state.
With SmartSearchInput
<smart-search-input> integrates via its input-name attribute and
getValue() method. SmartForm collects it as an array of selected item objects.
Use getSelectedIds() in event listeners if you need only the IDs.
<smart-form api-url="/api/projects/" client-validate > <smart-input type="text" name="project_name" label="Project Name" required></smart-input> <!-- input-name is the field key in the submitted payload --> <smart-search-input label="Assign Team Members" placeholder="Search people…" input-name="members" search-url="/api/users/?q=" show-badges allow-remove> </smart-search-input> <smart-input type="select" name="priority" label="Priority" data-options='[ {"id":"low","name":"Low"}, {"id":"medium","name":"Medium"}, {"id":"high","name":"High"} ]'></smart-input> <button type="submit">Create Project</button> </smart-form> // Payload — members is an array of full selected objects: // { project_name: "…", priority: "high", // members: [{id:1, name:"Alice"}, {id:3, name:"Bob"}] } // To read just the IDs from JS: const ids = document.querySelector('smart-search-input').getSelectedIds(); // → [1, 3]
With SmartQuill (Rich Text)
<smart-quill> exposes getValue() which returns the editor's
HTML content. SmartForm picks it up automatically under the field's name. The
value is an HTML string — your backend receives it directly in the JSON payload.
<smart-form api-url="/api/posts/" client-validate > <smart-input type="text" name="title" label="Post Title" required></smart-input> <smart-input type="select" name="category" label="Category" data-options='[ {"id":"tech","name":"Technology"}, {"id":"design","name":"Design"} ]'></smart-input> <!-- SmartQuill getValue() returns the HTML string --> <smart-quill name="body" label="Post Content" placeholder="Write your content here…" required> </smart-quill> <button type="submit">Publish Post</button> </smart-form> // Django view: // title = request.data["title"] // category = request.data["category"] // body = request.data["body"] # raw HTML string from Quill
Success Card
Add success-title and optionally success-subtitle to replace the
entire form with a success confirmation card on a successful response. Use
success-template to point to a <template> element for a
fully custom replacement. Use redirect-on-success for a page redirect instead.
<!-- Replace the form with a built-in success card --> <smart-form api-url="/api/waitlist/" client-validate success-title="🎉 You're on the list!" success-subtitle="We'll be in touch when access opens." > <smart-input type="email" name="email" label="Email" required></smart-input> <button type="submit">Request Access</button> </smart-form> <!-- Custom template replacement --> <smart-form api-url="/api/submit/" success-template="#my-success-tmpl" > ... </smart-form> <template id="my-success-tmpl"> <div class="custom-success-ui"> <h3>Done!</h3> <a href="/">Back to home</a> </div> </template> <!-- Or simply redirect --> <smart-form api-url="/api/submit/" redirect-on-success="/dashboard/" >...</smart-form>
Add Record + Refresh Table
Set refresh-target to the id of a <smart-table>
on the same page. After a successful submission SmartForm calls table.refresh()
automatically — the table re-fetches from its api-url and shows the newly
created row without a page reload.
<!-- Form — refresh-target matches the table id --> <smart-form api-url="/api/tags/" method="POST" client-validate refresh-target="tagsTable" > <smart-input type="text" name="tag_name" label="New Tag" required></smart-input> <smart-input type="select" name="color" label="Colour" data-options='[ {"id":"blue","name":"Blue"}, {"id":"green","name":"Green"} ]'></smart-input> <button type="submit">Add Tag</button> </smart-form> <!-- Table — id must match refresh-target above --> <smart-table id="tagsTable" api-url="/api/tags/" response-map='{"dataPath":"results","totalPath":"count"}' columns='[ {"field":"id","hidden":true}, {"field":"tag_name","label":"Tag"}, {"field":"color","label":"Colour","type":"badge"} ]' delete-api-url="/api/tags" page-size="20" ></smart-table> // After success: // 1. SmartForm fires a success toast // 2. SmartForm resets all fields (unless no-auto-reset) // 3. SmartForm calls tagsTable.refresh() — new row appears instantly
Django Integration
SmartForm reads the Django CSRF token from the csrftoken cookie automatically and
attaches it as X-CSRFToken in every AJAX request. Your Django REST view needs no
special handling — just @csrf_exempt is not required.
Use response-map to match your DRF serializer's error shape.
<!-- Template --> <smart-form api-url="<your-article-creation-api-url>" method="POST" client-validate response-map='{"successPath":"status","messagePath":"detail","errorsPath":"errors"}' refresh-target="articlesTable" > <smart-input type="text" name="title" label="Title" required></smart-input> <smart-input type="datepicker" name="pub_date" label="Publish Date"></smart-input> <smart-quill name="body" label="Content" required></smart-quill> <button type="submit">Publish</button> </smart-form> # DRF View — views.py class ArticleCreateView(generics.CreateAPIView): serializer_class = ArticleSerializer permission_classes = [IsAuthenticated] def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): serializer.save(author=request.user) return Response({ "status": "success", "message": "Article published.", "data": serializer.data, }) return Response({ "status": "error", "errors": serializer.errors, # mapped onto individual fields "message": "Please fix the errors below.", }, status=400)
When the view returns errors, SmartForm maps each key back to the matching
name attribute of the field. If a key has no matching field, the error appears
in the top-level error banner instead.
// Server returns 400 with field errors: { "status": "error", "errors": { "title": "This field is required.", "email": "Already taken." }, "message": "Please fix the errors below." } // SmartForm behaviour: // → Calls field.showError("This field is required.") on [name="title"] // → Calls field.showError("Already taken.") on [name="email"] // → If a key has no matching field, shows it in the top banner
Attributes
| Attribute | Type | Description | Default |
|---|---|---|---|
| api-url | string | AJAX POST target URL. When present and mode="auto", activates AJAX mode. |
— |
| action | string | Native <form action> URL. Used in native mode only. |
— |
| method | string | HTTP method for AJAX or native submission. | POST |
| mode | string | "ajax" — always AJAX. "native" — always native browser submit. "auto" — AJAX if api-url is set, native otherwise. |
auto |
| fetch-config | JSON | Extra fetch options. Supports headers (key/value pairs) and bodyMode: "json" | "form". CSRF token is always added automatically. |
{ bodyMode: "json" } |
| response-map | JSON | Maps your API's response shape. See Response Map section. | see below |
| client-validate | boolean | Run field.validate() on all fields before submitting. Blocks submission if any field is invalid. |
false |
| refresh-target | string | ID of a <smart-table> on the page. Calls table.refresh() after a successful submission. |
— |
| redirect-on-success | string | URL to navigate to after a successful AJAX submission. Takes priority over success card. | — |
| no-auto-reset | boolean | Disable automatic form.reset() on success. Useful for edit forms where you want values to persist. |
false |
| success-title | string | Replaces the form with a built-in success card showing this heading on success. | — |
| success-subtitle | string | Secondary text shown below success-title in the success card. |
— |
| success-template | string | CSS selector of a <template> element. Its content is cloned and replaces the form on success. |
— |
Response Map
response-map tells SmartForm how to read your API's JSON response.
All four paths use dot-notation and default gracefully when omitted.
| Key | Description | Default path |
|---|---|---|
| successPath | Dot path to the success flag. SmartForm reads this field and treats the response as success if the value is true, "success", "ok", or 1. Falls back to response.ok if the path is absent. |
status |
| messagePath | Dot path to the human-readable message string. Shown in the success toast or error banner. | message |
| errorsPath | Dot path to the errors object. Keys are matched against field name attributes. Unmatched keys appear in the top error banner. |
errors |
| dataPath | Dot path to the main data payload. Available in the smart-form-success event detail.data. |
— |
// Django REST Framework default shape: response-map='{"successPath":"status","messagePath":"detail","errorsPath":"errors"}' // Rails / simple JSON API shape: response-map='{"successPath":"ok","messagePath":"message","errorsPath":"field_errors"}' // Nested data — e.g. { "payload": { "status": "success", "user": {...} } } response-map='{"successPath":"payload.status","messagePath":"payload.message","dataPath":"payload.user"}' // No response-map needed if API returns HTTP 2xx = success, non-2xx = error: // SmartForm falls back to response.ok when successPath key is not found
Events
SmartForm dispatches two bubbling CustomEvents on the <smart-form> element.
Listen on the element or on any ancestor.
| Event | detail | Fires when |
|---|---|---|
| smart-form-success | { message, data } | The server responded with a success status. data is the parsed response object (or the value at dataPath if configured). |
| smart-form-error | { errors, message } | The server returned an error or the fetch failed. errors is the field-error object (may be empty), message is the fallback string. |
const form = document.querySelector('smart-form'); form.addEventListener('smart-form-success', (e) => { console.log('Saved:', e.detail.message); console.log('Response:', e.detail.data); // Example: update a badge counter after adding a new item const badge = document.querySelector('#item-count'); if (badge) badge.textContent = e.detail.data.total; }); form.addEventListener('smart-form-error', (e) => { console.warn('Error:', e.detail.message); console.warn('Fields:', e.detail.errors); });
Public API
All methods are available directly on the <smart-form> element.
| Method | Returns | Description |
|---|---|---|
| getValues() | Object | Returns a plain { fieldName: value } object for all fields inside the form. Calls each field's getValue() if available, falls back to .value. |
| setValues(obj) | void | Programmatically set field values. Calls each field's setValue(val) if available. Useful for pre-populating edit forms from JS. |
| submit() | void | Programmatically trigger form submission — equivalent to the user clicking the submit button. Respects client-validate. |
| reset() | void | Resets the native form and calls clear() on every SmartComponent field to clear values and validation state. |
const form = document.querySelector('#editForm'); // Read all current values (e.g. before a manual fetch) const payload = form.getValues(); // → { title: "Hello", status: "draft", body: "<p>...</p>", members: [{…}] } // Pre-populate from an API response (edit form) const article = await fetch('/api/articles/42/').then(r => r.json()); form.setValues({ title: article.title, status: article.status, body: article.body, }); // Trigger submit from an external button or keyboard shortcut document.getElementById('save-shortcut').addEventListener('click', () => { form.submit(); }); // Reset the form manually (e.g. Cancel button) document.getElementById('cancel-btn').addEventListener('click', () => { form.reset(); });
Field Contract
SmartForm discovers fields by scanning its subtree for elements that have a name
or input-name attribute and either expose getValue() or are native
HTML controls (input, select, textarea). All
SmartComponents already implement this contract. You can also use it to integrate your own
custom elements.
| Method / Prop | Required | Description |
|---|---|---|
| name / input-name attr | required | The field key that appears in getValues() output. Without it, the element is ignored by SmartForm. |
| getValue() | recommended | Returns the field's current value. Falls back to el.value for native elements. SmartInput, SmartQuill, and SmartSearchInput all implement this. |
| setValue(val) | optional | Called by form.setValues(). Falls back to assigning el.value = val. |
| validate() | optional | Called when client-validate is present. Should return true/false and show its own error state. Falls back to checkValidity() + reportValidity(). |
| showError(msg) | optional | Called by SmartForm when the server returns a field error for this field. Falls back to finding a sibling .invalid-feedback element. |
| clear() | optional | Called by form.reset(). Should clear the field value and any visible validation state. |
// Minimal custom element that SmartForm can collect and validate class MyCustomField extends HTMLElement { connectedCallback() { this.innerHTML = `<input type="text" />`; } // SmartForm reads this getValue() { return this.querySelector('input').value; } // SmartForm calls this for setValues() setValue(val) { this.querySelector('input').value = val; } // SmartForm calls this when client-validate is set validate() { return this.getValue().length > 0; } // SmartForm calls this when the server returns a field error showError(msg) { /* render msg inline */ } // SmartForm calls this on reset() clear() { this.querySelector('input').value = ''; } } customElements.define('my-custom-field', MyCustomField); <!-- Usage inside smart-form: --> <smart-form api-url="/api/save/"> <my-custom-field name="custom_field"></my-custom-field> <button type="submit">Save</button> </smart-form>
Forbidden Elements
custom-submit-button and icon-button[post] must not
be placed inside <smart-form>. Both components manage their own AJAX
submission independently, which conflicts with SmartForm's submit interception.
SmartForm detects these at connect time, logs a console error, and disables itself entirely
to prevent double-submission bugs.
// ✕ WRONG — custom-submit-button and icon-button[post] fight over submit <smart-form api-url="/api/save/"> <smart-input name="title" ...></smart-input> <custom-submit-button post="/api/save/"></custom-submit-button> </smart-form> // → SmartForm logs an error and disables itself // ✓ CORRECT — use a plain <button type="submit"> <smart-form api-url="/api/save/"> <smart-input name="title" ...></smart-input> <button type="submit">Save</button> </smart-form>