Forms & Input
Advanced Forms (fieldset, datalist, output)
Master advanced form techniques for complex user interfaces. Learn custom validation, dynamic forms, file uploads, and progressive enhancement strategies.
Custom Form Validation
Beyond HTML5's built-in validation, create custom validation logic.
Using setCustomValidity()
HTML
<form id="signup-form">
<label for="password">Password:</label>
<input type="password" id="password" name="password" required>
<label for="confirm">Confirm Password:</label>
<input type="password" id="confirm" name="confirm" required>
<button type="submit">Sign Up</button>
</form>
<script>
const form = document.getElementById('signup-form');
const password = document.getElementById('password');
const confirm = document.getElementById('confirm');
confirm.addEventListener('input', () => {
if (password.value !== confirm.value) {
confirm.setCustomValidity('Passwords must match');
} else {
confirm.setCustomValidity(''); // Clear error
}
});
form.addEventListener('submit', (e) => {
if (!form.checkValidity()) {
e.preventDefault();
// Show custom error messages
}
});
</script>Validity States
HTML
<script>
const input = document.querySelector('input');
// Check validity states
if (input.validity.valueMissing) {
console.log('Field is required');
}
if (input.validity.typeMismatch) {
console.log('Value doesn't match type (e.g., invalid email)');
}
if (input.validity.patternMismatch) {
console.log('Value doesn't match pattern');
}
if (input.validity.tooShort) {
console.log('Value is too short');
}
if (input.validity.tooLong) {
console.log('Value is too long');
}
if (input.validity.rangeUnderflow) {
console.log('Value is below min');
}
if (input.validity.rangeOverflow) {
console.log('Value is above max');
}
if (input.validity.stepMismatch) {
console.log('Value doesn't fit step');
}
if (input.validity.customError) {
console.log('Custom validation error');
}
// Overall validity
console.log('Is valid:', input.validity.valid);
console.log('Validation message:', input.validationMessage);
</script>Dynamic Form Fields
Add/Remove Fields Dynamically
HTML
<form id="skills-form">
<fieldset>
<legend>Skills</legend>
<div id="skills-container">
<div class="skill-entry">
<input type="text" name="skills[]" placeholder="Skill name">
<button type="button" class="remove-skill">Remove</button>
</div>
</div>
<button type="button" id="add-skill">Add Skill</button>
</fieldset>
<button type="submit">Submit</button>
</form>
<script>
const container = document.getElementById('skills-container');
const addButton = document.getElementById('add-skill');
addButton.addEventListener('click', () => {
const entry = document.createElement('div');
entry.className = 'skill-entry';
entry.innerHTML = `
<input type="text" name="skills[]" placeholder="Skill name">
<button type="button" class="remove-skill">Remove</button>
`;
container.appendChild(entry);
});
container.addEventListener('click', (e) => {
if (e.target.classList.contains('remove-skill')) {
if (container.children.length > 1) {
e.target.parentElement.remove();
}
}
});
</script>File Upload Advanced
Preview Images Before Upload
HTML
<input type="file" id="photo" accept="image/*">
<img id="preview" style="display:none; max-width: 300px;">
<script>
const fileInput = document.getElementById('photo');
const preview = document.getElementById('preview');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
preview.src = e.target.result;
preview.style.display = 'block';
};
reader.readAsDataURL(file);
}
});
</script>Multiple File Upload with Details
HTML
<input type="file" id="files" multiple>
<div id="file-list"></div>
<script>
const fileInput = document.getElementById('files');
const fileList = document.getElementById('file-list');
fileInput.addEventListener('change', (e) => {
fileList.innerHTML = '';
Array.from(e.target.files).forEach(file => {
const item = document.createElement('div');
item.innerHTML = `
<strong>${file.name}</strong>
<span>${(file.size / 1024).toFixed(2)} KB</span>
<span>Type: ${file.type}</span>
`;
fileList.appendChild(item);
});
});
</script>Drag and Drop File Upload
HTML
<div id="drop-zone">
Drag files here or click to upload
<input type="file" id="file-input" multiple style="display:none;">
</div>
<script>
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const files = e.dataTransfer.files;
handleFiles(files);
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
function handleFiles(files) {
console.log('Files:', files);
// Process files...
}
</script>
<style>
#drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 40px;
text-align: center;
cursor: pointer;
transition: background-color 0.3s;
}
#drop-zone.drag-over {
background-color: #e3f2fd;
border-color: #2196F3;
}
</style>Form Submission Handling
Prevent Default & Handle with JavaScript
HTML
<form id="contact-form">
<input type="text" name="name" required>
<input type="email" name="email" required>
<textarea name="message" required></textarea>
<button type="submit">Send</button>
<div id="status"></div>
</form>
<script>
const form = document.getElementById('contact-form');
const status = document.getElementById('status');
form.addEventListener('submit', async (e) => {
e.preventDefault(); // Prevent default form submission
const formData = new FormData(form);
try {
status.textContent = 'Sending...';
const response = await fetch('/api/contact', {
method: 'POST',
body: formData
});
if (response.ok) {
status.textContent = 'Message sent successfully!';
form.reset();
} else {
status.textContent = 'Error sending message.';
}
} catch (error) {
status.textContent = 'Network error.';
console.error(error);
}
});
</script>FormData API
HTML
<script>
const form = document.querySelector('form');
const formData = new FormData(form);
// Get values
console.log(formData.get('name'));
console.log(formData.get('email'));
// Add/Set values
formData.append('timestamp', Date.now());
formData.set('status', 'draft');
// Delete values
formData.delete('temp');
// Check if field exists
if (formData.has('name')) {
console.log('Name field exists');
}
// Iterate over entries
for (let [key, value] of formData.entries()) {
console.log(key, value);
}
// Convert to object
const data = Object.fromEntries(formData);
console.log(data);
// Convert to JSON
const json = JSON.stringify(Object.fromEntries(formData));
</script>Multi-Step Forms
HTML
<form id="multi-step-form">
<!-- Step 1 -->
<div class="form-step active" data-step="1">
<h2>Step 1: Personal Info</h2>
<input type="text" name="name" required>
<input type="email" name="email" required>
<button type="button" class="next-step">Next</button>
</div>
<!-- Step 2 -->
<div class="form-step" data-step="2">
<h2>Step 2: Address</h2>
<input type="text" name="address" required>
<input type="text" name="city" required>
<button type="button" class="prev-step">Previous</button>
<button type="button" class="next-step">Next</button>
</div>
<!-- Step 3 -->
<div class="form-step" data-step="3">
<h2>Step 3: Review</h2>
<div id="review"></div>
<button type="button" class="prev-step">Previous</button>
<button type="submit">Submit</button>
</div>
<!-- Progress -->
<div class="progress">
<div class="progress-bar" style="width: 33%"></div>
</div>
</form>
<script>
const steps = document.querySelectorAll('.form-step');
const progressBar = document.querySelector('.progress-bar');
let currentStep = 1;
function showStep(stepNumber) {
steps.forEach(step => {
step.classList.remove('active');
if (parseInt(step.dataset.step) === stepNumber) {
step.classList.add('active');
}
});
progressBar.style.width = (stepNumber / steps.length * 100) + '%';
currentStep = stepNumber;
}
document.querySelectorAll('.next-step').forEach(btn => {
btn.addEventListener('click', () => {
// Validate current step
const currentFields = steps[currentStep - 1].querySelectorAll('[required]');
let valid = true;
currentFields.forEach(field => {
if (!field.checkValidity()) {
field.reportValidity();
valid = false;
}
});
if (valid && currentStep < steps.length) {
showStep(currentStep + 1);
}
});
});
document.querySelectorAll('.prev-step').forEach(btn => {
btn.addEventListener('click', () => {
if (currentStep > 1) {
showStep(currentStep - 1);
}
});
});
</script>
<style>
.form-step {
display: none;
}
.form-step.active {
display: block;
}
.progress {
height: 4px;
background-color: #e0e0e0;
margin: 20px 0;
}
.progress-bar {
height: 100%;
background-color: #4CAF50;
transition: width 0.3s;
}
</style>Form Auto-save
HTML
<form id="auto-save-form">
<textarea name="content" rows="10" cols="50"></textarea>
<p id="save-status">Changes auto-saved</p>
</form>
<script>
const form = document.getElementById('auto-save-form');
const status = document.getElementById('save-status');
let saveTimeout;
form.addEventListener('input', () => {
status.textContent = 'Unsaved changes...';
// Debounce save
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
saveFormData();
}, 2000); // Save after 2 seconds of inactivity
});
function saveFormData() {
const formData = new FormData(form);
// Save to localStorage
for (let [key, value] of formData.entries()) {
localStorage.setItem(`form_${key}`, value);
}
status.textContent = 'Changes auto-saved ✓';
}
// Load saved data on page load
window.addEventListener('load', () => {
form.querySelectorAll('[name]').forEach(field => {
const saved = localStorage.getItem(`form_${field.name}`);
if (saved) {
field.value = saved;
}
});
});
</script>Accessible Error Messages
HTML
<form>
<div class="form-group">
<label for="email">Email:</label>
<input type="email"
id="email"
name="email"
aria-describedby="email-error"
required>
<span id="email-error"
class="error-message"
role="alert"
aria-live="polite"></span>
</div>
<button type="submit">Submit</button>
</form>
<script>
const email = document.getElementById('email');
const errorMsg = document.getElementById('email-error');
email.addEventListener('blur', () => {
if (!email.validity.valid) {
email.setAttribute('aria-invalid', 'true');
errorMsg.textContent = email.validationMessage;
} else {
email.setAttribute('aria-invalid', 'false');
errorMsg.textContent = '';
}
});
</script>
<style>
input[aria-invalid="true"] {
border-color: #f44336;
}
.error-message {
color: #f44336;
font-size: 0.875em;
display: block;
margin-top: 4px;
}
</style>Best Practices
✅ Do
- Validate on both client and server
- Provide clear error messages
- Use ARIA for accessibility
- Auto-save long forms
- Show progress in multi-step forms
- Disable submit while processing
- Handle errors gracefully
- Test keyboard navigation
⌠Don't
- Rely only on client-side validation
- Show cryptic error messages
- Submit forms without feedback
- Make forms unnecessarily long
- Forget mobile users
- Block form re-submission always
- Lose user data on errors
- Forget accessibility