Modular Forms: Building Scalable Web Forms
Creating complex forms can quickly become a maintenance nightmare. By breaking a form down into reusable, self‑contained modules, you gain flexibility, readability, and testability. This post walks through the core concepts of modular forms and provides a live example using vanilla JavaScript and modern HTML.
Why Modular Forms?
- Reusability: The same input component can be used across multiple pages.
- Isolation: Each module owns its own validation and UI state.
- Scalability: Adding new fields or whole sections doesn’t require rewriting the entire form.
Core Building Blocks
1. Form Field Component
<template id="field-template">
<div class="field">
<label><span class="label-text"></span></label>
<input class="input" />
<div class="error"></div>
</div>
</template>
<script>
class FormField extends HTMLElement {
constructor() {
super();
const template = document.getElementById('field-template');
const clone = template.content.cloneNode(true);
this.attachShadow({mode:'open'}).appendChild(clone);
}
connectedCallback() {
const label = this.getAttribute('label') || '';
const type = this.getAttribute('type') || 'text';
this.shadowRoot.querySelector('.label-text').textContent = label;
const input = this.shadowRoot.querySelector('.input');
input.type = type;
input.name = this.getAttribute('name') || '';
input.required = this.hasAttribute('required');
input.addEventListener('input', () => this.validate());
}
validate() {
const input = this.shadowRoot.querySelector('.input');
const errorDiv = this.shadowRoot.querySelector('.error');
if (input.validity.valid) {
errorDiv.textContent = '';
this.dispatchEvent(new CustomEvent('field-valid', {detail:{name:input.name,value:input.value}}));
} else {
errorDiv.textContent = input.validationMessage;
}
}
}
customElements.define('form-field', FormField);
</script>
2. Form Container
<form id="contact-form">
<form-field label="Name" name="name" required></form-field>
<form-field label="Email" name="email" type="email" required></form-field>
<form-field label="Message" name="message" type="textarea" required></form-field>
<button type="submit">Send</button>
</form>
<script>
const form = document.getElementById('contact-form');
form.addEventListener('submit', e => {
e.preventDefault();
const formData = new FormData(form);
console.log('Submitted', Object.fromEntries(formData));
alert('Form submitted! Check console for data.');
});
</script>