Forms

Forms provide a comprehensive set of form controls and layouts. Includes inputs, textareas, selects, checkboxes, radios, switches, and validation styling. All components work with native HTML form elements enhanced with Domma's theme-aware styling.

Key Features

  • Native HTML - Works with standard form elements
  • Validation States - Success, error, and warning styling
  • Input Groups - Addons, icons, and button combinations
  • Layouts - Vertical, horizontal, and inline forms

Basic Usage


<div class="form-group">
    <label>Email</label>
    <input type="email" class="form-input" placeholder="you@example.com">
</div>

Quick Start

Simple Form


<form>
    <div class="form-group">
        <label>Email</label>
        <input type="email" class="form-input" placeholder="you@example.com">
    </div>
    <div class="form-group">
        <label>Password</label>
        <input type="password" class="form-input">
    </div>
    <button type="submit" class="btn btn-primary">Sign In</button>
</form>

Tutorial

Build a registration form with validation.

Enter your first and last name
Must be at least 8 characters

Fill out the form and submit


document.getElementById('form').addEventListener('submit', (e) => {
    e.preventDefault();

    // Clear previous validation
    document.querySelectorAll('.form-group').forEach(group => {
        group.classList.remove('has-error', 'has-success');
    });

    // Validate name
    const name = document.getElementById('name');
    if (name.value.length < 2) {
        name.closest('.form-group').classList.add('has-error');
        return;
    }
    name.closest('.form-group').classList.add('has-success');

    // Validate email
    const email = document.getElementById('email');
    if (!email.value.includes('@')) {
        email.closest('.form-group').classList.add('has-error');
        return;
    }
    email.closest('.form-group').classList.add('has-success');

    // Form is valid
    console.log('Form submitted successfully!');
});

Examples

Input Types

Select Menus

Checkboxes and Radios

Input Sizes

Validation States

Looks good!
This field is required
Are you sure?

Input Groups

@
.00

Inline Forms

Best Practices

Accessibility

  • Labels - Always provide elements for inputs
  • ARIA Attributes - Use aria-required, aria-invalid for validation
  • Error Messages - Link errors to inputs using aria-describedby
  • Keyboard Navigation - Ensure all form controls are keyboard accessible

Validation

  • Client-Side First - Validate on submit before sending to server
  • Clear Feedback - Use validation states and helper text
  • Real-Time Validation - Consider validating on blur for better UX
  • Server-Side Required - Always validate on backend for security

UX Guidelines

  • Placeholder Text - Use for examples, not instructions
  • Help Text - Provide context with .form-text
  • Logical Grouping - Group related fields together
  • Required Fields - Mark clearly with asterisk or label
  • Button States - Disable submit while processing

CSS Classes

Class Description
.form-group Form field container
.form-input Text input styling
.form-textarea Textarea styling
.form-select Select dropdown styling
.form-checkbox Checkbox wrapper
.form-radio Radio button wrapper
.form-text Helper text
.form-input-sm Small input size
.form-input-lg Large input size
.has-success Success validation state
.has-error Error validation state
.has-warning Warning validation state
.input-group Input group container
.input-group-addon Input group prefix/suffix
.form-inline Inline form layout

Hybrid CSS + JavaScript

Forms work perfectly with pure CSS styling, but you can enhance them with JavaScript for validation, auto-save, dynamic fields, and more using Domma's declarative configuration.

Form Validation with $.setup()

$.setup({
    '#registration-form': {
        initial: {
            data: { touched: {}, errors: {} }
        },
        events: {
            submit: (e, $form) => {
                e.preventDefault();

                // Collect form data
                const formData = {
                    username: $form.find('[name="username"]').val(),
                    email: $form.find('[name="email"]').val(),
                    password: $form.find('[name="password"]').val()
                };

                // Validate with utils
                const errors = {};
                if (_.isEmpty(_.trim(formData.username))) {
                    errors.username = 'Username required';
                }
                if (!formData.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
                    errors.email = 'Invalid email';
                }
                if (formData.password.length < 8) {
                    errors.password = 'Password must be 8+ characters';
                }

                $form.data('errors', errors);

                if (_.isEmpty(errors)) {
                    // Submit
                    Domma.http.post('/api/register', formData)
                        .then(() => {
                            Domma.elements.toast.success('Registered successfully!');
                            window.location = '/dashboard';
                        })
                        .catch(err => {
                            Domma.elements.toast.error(err.message);
                        });
                } else {
                    // Show errors
                    _.forEach(errors, (msg, field) => {
                        $form.find(`[name="${field}"]`)
                            .closest('.form-group')
                            .addClass('has-error')
                            .find('.error-message').text(msg);
                    });
                }
            },

            'blur .form-input': _.debounce((e, $form) => {
                const $input = $(e.target);
                const field = $input.attr('name');
                const value = $input.val();
                const $group = $input.closest('.form-group');

                // Mark as touched
                const touched = $form.data('touched') || {};
                touched[field] = true;
                $form.data('touched', touched);

                // Validate
                let isValid = true;
                if (field === 'email') {
                    isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
                } else if (field === 'password') {
                    isValid = value.length >= 8;
                } else {
                    isValid = !_.isEmpty(_.trim(value));
                }

                $group.toggleClass('has-error', !isValid);
                $group.toggleClass('has-success', isValid);
            }, 300)
        }
    }
});

Auto-Save Draft

$.setup({
    '#article-form': {
        initial: {
            data: { saveTimer: null, lastSaved: null }
        },
        events: {
            'input .form-input, change select': _.debounce((e, $form) => {
                // Collect current state
                const data = {
                    title: $form.find('[name="title"]').val(),
                    content: $form.find('[name="content"]').val(),
                    category: $form.find('[name="category"]').val()
                };

                // Save draft
                Domma.http.post('/api/drafts/auto-save', data)
                    .then(() => {
                        $form.data('lastSaved', new Date());
                        $('#save-status').text('Draft saved ' + D().format('HH:mm:ss'));
                    });
            }, 2000)
        }
    }
});

Dynamic Form Fields

$.setup({
    '#contact-form': {
        events: {
            'click #add-phone': (e, $form) => {
                const phoneCount = $form.find('.phone-input').length;
                const html = `
                    <div class="form-group">
                        <input type="tel" name="phone_${phoneCount}"
                               class="form-input phone-input"
                               placeholder="Phone ${phoneCount + 1}">
                    </div>
                `;
                $form.find('#phone-inputs').append(html);
            },

            'change [name="contact-method"]': (e, $form) => {
                const method = $(e.target).val();

                // Show/hide relevant fields
                $form.find('.email-field').toggle(method === 'email');
                $form.find('.phone-field').toggle(method === 'phone');
                $form.find('.address-field').toggle(method === 'mail');
            }
        }
    }
});

Multi-Step Form

$.setup({
    '#wizard-form': {
        initial: {
            data: { currentStep: 1, totalSteps: 3, formData: {} }
        },
        events: {
            'click .next-step': (e, $form) => {
                const data = $form.data();
                if (data.currentStep < data.totalSteps) {
                    // Collect current step data
                    $form.find(`.step-${data.currentStep} .form-input`).each(function() {
                        const $input = $(this);
                        data.formData[$input.attr('name')] = $input.val();
                    });

                    // Move to next step
                    data.currentStep++;
                    $form.data(data);

                    // Update UI
                    $form.find('.step').hide();
                    $form.find(`.step-${data.currentStep}`).fadeIn();

                    $('#step-indicator').text(`Step ${data.currentStep} of ${data.totalSteps}`);
                }
            },

            'click .prev-step': (e, $form) => {
                const data = $form.data();
                if (data.currentStep > 1) {
                    data.currentStep--;
                    $form.data(data);

                    $form.find('.step').hide();
                    $form.find(`.step-${data.currentStep}`).fadeIn();

                    $('#step-indicator').text(`Step ${data.currentStep} of ${data.totalSteps}`);
                }
            }
        }
    }
});

Benefits of Hybrid Approach

  • Progressive Enhancement - Forms work without JS, enhanced with JS
  • Better UX - Real-time validation, auto-save, dynamic fields
  • Maintainability - Centralized logic with $.setup()
  • Accessibility - CSS provides base styling, JS adds interactivity

CSS-Only vs CSS+JS Hybrid

CSS-Only (Default)

<form action="/submit" method="POST">
    <div class="form-group">
        <label>Email</label>
        <input type="email"
               class="form-input"
               name="email"
               required>
    </div>

    <button type="submit"
            class="btn btn-primary">
        Submit
    </button>
</form>

<!-- Works with native validation -->

CSS+JS Hybrid (Enhanced)

<form id="enhanced-form">
    <div class="form-group">
        <label>Email</label>
        <input type="email"
               class="form-input"
               name="email">
        <span class="error-message"></span>
    </div>

    <button type="submit"
            class="btn btn-primary">
        Submit
    </button>
</form>

<script>
$.setup({
    '#enhanced-form': {
        events: {
            submit: validateAndSubmit,
            'blur .form-input': validateField
        }
    }
});
</script>

Related Elements