Forms & Buttons

Mode

Skin

Theme

Overview & Philosophy

ln-form is applied to standard HTML forms via <form data-ln-form action="..." method="...">. It listens for ln-fill events carrying record payloads to populate matching controls ([name="key"]) and dispatches synthetic input and change events so reactive components (like ln-validate and ln-autoresize) update in sync. In edit workflows, it automatically ensures an <input type="hidden" name="_method" value="PUT"> exists for RESTful method spoofing, without intercepting native form submission.

In ln-ashlar, forms are built on a strict, platform-first **Coordinator & Primitive** doctrine: upgrading native HTML forms instead of inventing a custom JavaScript state engine.

The Decoupled Form Doctrine

The form layer has zero direct references to layout widths or validation logic. JavaScript only manages the high-level gate (submit, fill, reset), while individual fields own their state via the platform's ValidityState API. The DOM is the single source of truth.

Three Orthogonal Layers of Form UX

  1. The Layout Engine (CSS Grid Mixins): Responsive multi-column layouts are handled purely in SCSS using the @include form-grid mixin. Column spans are defined inside form-specific stylesheets via semantic nth-child selectors. The HTML contains no layout classes (like .col-md-6 or .form-row), keeping markup clean and flexible.
  2. The Validity Primitive (ln-validate): Each input, select, or textarea is a standalone validation primitive. It wraps the browser's native ValidityState, controls when to surface errors visually (only after the field is touched), and manages custom/server-side errors via the ln-validate:set-custom event channel.
  3. The Form Coordinator (ln-form): This is the coordinator. It sits on the root <form> element and manages fill/reset and, for local-first write pages, the submit validation gate (blocks invalid POST/PUT/PATCH submits on any data-ln-form form; novalidate is injected automatically by ln-validate wherever a field needs it). Field-level validation state is ln-validate's job; a valid submit is claimed by ln-data-coordinator, not dispatched by ln-form.

Data and Validation Flow

When a user interacts with a form:

  1. The user types in a field. The field's ln-validate primitive catches the input or change event, marks the field as touched, evaluates validity using dom.checkValidity(), updates the input's visual classes (.ln-validate-valid / .ln-validate-invalid), toggles the matching error in the error list, and dispatches a bubbled ln-validate:valid or ln-validate:invalid CustomEvent.
  2. The event bubbles up to the root [data-ln-form]. The coordinator intercepts it, scans the active fields, and re-calculates the submit button's disabled state.
  3. Without data-ln-data-coordinator-scope, submit is left entirely native — the browser submits to action/method as usual, or a separate ajax component may intercept the native submit event. With data-ln-data-coordinator-scope, a valid submit is still left native — the matching ln-data-coordinator claims it via preventDefault() (document, bubble phase) and serializes the form itself.

Why This Decoupled Approach is Superior

  • Ultra-Lightweight & Responsive: Because validation is delegated to the browser's native engine, there is no sluggish JS-based validation loop running on every keystroke. Typing remains instantaneous.
  • Seamless Server Integration: Server-side validation errors are easily mapped back to fields using standard ln-validate:set-custom events. The coordinator and form are agnostic to where errors originate.
  • Unified Execution on Fill/Reset: Programmatic bulk population (via lnForm.fill()) fires synthetic events, ensuring textareas resize instantly, fields re-validate, and the submit button enables itself.

Anatomy & Rules

Each field is a <div class="form-element"> containing a <label for="..."> and <input id="..."> (explicit association). The root <form> gets @include form-grid in SCSS — a 6-column grid, collapsing to 1 column on mobile. Column widths are set via nth-child in form-specific SCSS.

Element Rule / Attribute Description
Root Form <form data-ln-form> Creates the form coordinator instance. Styled with @include form-grid.
Field Wrapper <div class="form-element"> Wraps the label, input control, and error list. Controls alignment and column spans.
Label <label for="my-id"> Explicit association with input's ID for premium accessibility (clicks focus input).
Input / Control <input id="my-id" data-ln-validate> The native control. Rules like required or type="email" reside natively here.
Error List <ul data-ln-validate-errors> Container for <li class="hidden" data-ln-validate-error="rule"> error messages.
Button Group <ul class="form-actions"> Handles form action button wrapping. Cancel is type="button"; Save is type="submit".

Live demo — ln-validate + ln-form

Event log


					

Fill — edit mode

Populate form fields programmatically or declaratively. Three patterns:

Code

// Pattern 1: Direct API — caller has a reference to the form
form.lnForm.fill({ fname: 'Dalibor', email: 'd@test.com' });

// Pattern 2: Coordinator — routes external events to the form
// Table doesn't know about the form. Form doesn't know about the table.
// The coordinator is the glue.
table.addEventListener('ln-table:row-click', function (e) {
    form.lnForm.fill(e.detail.record);
});

// Pattern 3: Declarative (HTML only) — using data-ln-fill-form and data-ln-fill-*
// Trigger:
<button type="button"
        data-ln-fill-form="my-form"
        data-ln-fill-fname="Dalibor"
        data-ln-fill-email="d@test.com">
    Fill Form
</button>

// Reset
form.lnForm.reset();

Filled Pills (Joined Group)

Joined horizontal segmented control using ul.pills. Corner radiuses are automatically adjusted for a cohesive, joined horizontal block.

Radio selection

Checkbox selection

Outline Pills

Outline variant with transparent background, visible input indicator, and border highlighting. Can be grouped horizontally or used as a standalone label choice.

Grouped Outline (ul.pills-outline)

Standalone Outline (label.pill-outline)

Switch Pills (iOS Toggle Switches)

iOS-style sliding switches for immediate on/off preferences. Styled purely via CSS classes on the ul or standalone label.

Settings Stack (ul.pills-switch)

Disabled State

Standalone Switch (label.pill-switch)

Input States

  • Invalid email address

Select & Textarea

Checkboxes & Radios

Checkboxes
Radios

Buttons — Architecture

Three tiers. No class needed for the first two — the browser type attribute determines the role.

1. Neutral — <button type="button">

All buttons get structure (padding, font, flex, hover) from _global.scss. Cancel/Close/Toggle buttons need no class.

2. Primary — type="submit"

Submit buttons automatically receive primary fill color. Structure inherited from tier 1. No class needed.

3. Action button — class="btn"

Non-submit action buttons (Add, Delete, Export). In production, apply via @include btn on a semantic selector in project SCSS. class="btn" is for prototyping only.

Color change — override --color-primary

Set --color-primary on the button or any parent. All states (hover, active, focus-ring) adapt automatically.

/* Project SCSS — semantic selector */
#delete-user  { @include btn; --color-primary: var(--color-error); }
#approve-user { @include btn; --color-primary: var(--color-success); }

/* Context override — all submit + action buttons inside change */
.danger-zone  { --color-primary: var(--color-error); }

Button Sizes

Size modifiers: class="btn btn-sm" and class="btn btn-lg". In projects, apply via @include btn-sm / @include btn-lg after @include btn.

Icon-only Buttons

Icon-only buttons use no class — they get neutral global styling (transparent, gray hover). No padding override needed.

Button Groups — btn-group

Row action buttons grouped with small gap. In projects: #users-table .row-actions { @include btn-group; }

Implementation

HTML

<form id="my-form" data-ln-form>

  <div class="form-element">
    <label for="name">Name</label>
    <input type="text" id="name" name="name" required data-ln-validate>
    <ul data-ln-validate-errors>
      <li class="hidden" data-ln-validate-error="required">Required field</li>
    </ul>
  </p>

  <div class="form-element">
    <label for="email">Email</label>
    <input type="email" id="email" name="email" required data-ln-validate>
    <ul data-ln-validate-errors>
      <li class="hidden" data-ln-validate-error="required">Required</li>
      <li class="hidden" data-ln-validate-error="typeMismatch">Invalid email</li>
    </ul>
  </p>

  <div class="form-element">
    <label for="category">Category</label>
    <select id="category" name="category">
      <option value="a">Option A</option>
    </select>
  </p>

  <ul class="form-actions">
    <li><button type="button">Cancel</button></li>
    <li><button type="submit">Save</button></li>
  </ul>

</form>

SCSS

#my-form {
  @include form-grid;

  .form-element { grid-column: span 3; }                   // default: half
  .form-element:nth-child(3) { grid-column: span 6; }     // category: full
  > fieldset { grid-column: span 6; }
  .form-actions { grid-column: span 6; }
}

Coordinator (local-first write pages)

<form id="my-form" data-ln-form data-ln-data-coordinator-scope="my-resource"
      method="post" action="/api/resource" data-ln-form-action-edit>

// The nearest (or named) [data-ln-data-coordinator] claims
// the native submit automatically (preventDefault) — no application JS
// needed to build request-create/request-update events by hand. React to
// the store outcome instead of a form-level event:
document.addEventListener('ln-data-store:created', function (e) {
    if (e.detail.store !== 'my-resource') return;
    document.getElementById('my-modal').setAttribute('data-ln-modal', 'close');
});

focus-ring — outer glow (default)

Safe, universally readable. No layout shift. Tailwind / Vercel convention.

        focus-border-thicken — structural outline

        Sharp, structural. No glow. Enterprise / business UI. Material Design style.

              focus-combination — border + ring

              Border color + outer ring. Maximum signal strength. Accessibility-first.

                    focus-background-shift — minimal tint

                    Very light primary tint. No border change. Focus is felt, not announced.

                          focus-accent-line — bottom border only

                          Bottom border only changes. Subtle. Good inside bordered containers.

                                focus-inset-shadow — tactile sink

                                Inner shadow — field visually sinks. Tactile, three-dimensional feel.

                                      RESTful action routing — create vs. edit

                                      Add data-ln-form-action-edit to opt in. On fill with a record that has an id, ln-form rewrites form.action to the per-record URL and injects a hidden <input name="_method">. On new (no id), it restores the base action and clears _method. No coordinator JS required.

                                      Live action readout

                                      Click a trigger to see the DOM rewrite.

                                      Event log

                                      
                                      
                                      		
                                      	

                                      New package Edit package #