<smart-form>

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.

AJAX + Native Auto CSRF Client Validation Field Error Mapping Table Refresh Success UI
<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

AttributeTypeDescriptionDefault
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.

KeyDescriptionDefault 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.

EventdetailFires 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.

MethodReturnsDescription
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 / PropRequiredDescription
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>