47. Forms API: Advanced Form Handling in JavaScript
Introduction
The Forms API provides powerful ways to interact with HTML form elements. With the FormData object, you can easily collect, manipulate, and submit form data. The Forms API also provides built-in validation and various methods to control form submission.
For IT students building a campus portal, understanding the Forms API is crucial. Graduation registration forms, course registration forms, and assignment submission forms all need robust handling.
Why Is the Forms API Important?
Forms are the primary way users interact with web applications. In a digital campus, there are many forms that must be handled correctly:
- Graduation registration form with complex validation
- Online course registration form
- Assignment submission form with file upload
- Lecturer evaluation form
- Facility borrowing form
Without a solid understanding of the Forms API, form handling becomes chaotic, with inconsistent validation and error-prone submission.
Accessing Form Elements
// Access a form by ID
const form = document.getElementById('registration-form');
// Access form elements by name
const nameInput = form.elements['full_name'];
const emailInput = form.elements.email;
// Iterate over all form elements
for (const element of form.elements) {
console.log(`${element.name}: ${element.value}`);
}
// Access all inputs with a specific type
const textInputs = form.querySelectorAll('input[type="text"]');
const selects = form.querySelectorAll('select');
const textareas = form.querySelectorAll('textarea');
FormData Object
FormData is the modern way to collect form data:
// Create a FormData from a form
const form = document.getElementById('registration-form');
const formData = new FormData(form);
// Access the data
const name = formData.get('name');
const allValues = formData.getAll('hobbies'); // For checkboxes
// Add data manually
formData.append('timestamp', Date.now());
formData.append('source', 'web');
// Remove data
formData.delete('optional_field');
// Check whether a field exists
if (formData.has('email')) {
console.log('Email field ada');
}
// Iterate over all data
for (const [key, value] of formData) {
console.log(`${key}: ${value}`);
}
// Convert to a plain object
const dataObject = Object.fromEntries(formData.entries());
console.log(dataObject);
// Convert to JSON
const jsonData = JSON.stringify(Object.fromEntries(formData.entries()));
// Send with fetch
fetch('/api/registration', {
method: 'POST',
body: formData // FormData automatically sets Content-Type: multipart/form-data
}).then(res => res.json())
.then(data => console.log(data));
The Submit Event and preventDefault()
const form = document.getElementById('registration-form');
// Catch the submit event
form.addEventListener('submit', (event) => {
// Prevent the default submit (page reload)
event.preventDefault();
// Collect the data
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
// Validate manually if needed
if (!validateForm(data)) {
return;
}
// Send the data
submitRegistration(data);
});
// Validation example
function validateForm(data) {
const errors = [];
if (!data.name || data.name.trim().length < 3) {
errors.push('Name must be at least 3 characters');
}
if (!data.email || !data.email.endsWith('@university.edu')) {
errors.push('Email must use the university domain');
}
if (!data.studentId || !/^\d{10}$/.test(data.studentId)) {
errors.push('StudentID must be 10 digits');
}
if (errors.length > 0) {
showErrors(errors);
return false;
}
return true;
}
function showErrors(errors) {
const errorContainer = document.getElementById('form-errors');
errorContainer.innerHTML = errors.map(err => `<p class="error">${err}</p>`).join('');
errorContainer.style.display = 'block';
}
form.reset()
// Reset the form to its initial values
const form = document.getElementById('registration-form');
form.reset();
// Reset specific fields
function resetForm() {
form.reset();
// Reset custom state
document.getElementById('file-preview').innerHTML = '';
document.getElementById('error-messages').innerHTML = '';
document.getElementById('submit-btn').disabled = false;
// Reset validation states
form.querySelectorAll('.is-valid, .is-invalid').forEach(el => {
el.classList.remove('is-valid', 'is-invalid');
});
}
requestSubmit()
// requestSubmit() triggers the submit event and can accept a submitter
const form = document.getElementById('registration-form');
// Submit with a specific button as the submitter
const submitBtn = document.getElementById('submit-btn');
form.requestSubmit(submitBtn);
// requestSubmit() also invokes the browser's built-in validation
// Unlike form.submit(), which does not trigger the submit event
form.requestSubmit(); // Triggers validation + the submit event
// Difference from form.submit()
form.submit(); // Does not trigger the submit event, no validation
checkValidity() and Built-in Validation
const form = document.getElementById('registration-form');
// Check the validity of the entire form
const isValid = form.checkValidity();
console.log('Form valid:', isValid);
// Check the validity of a specific field
const emailInput = form.elements.email;
console.log('Email valid:', emailInput.checkValidity());
// Get all invalid elements
const invalidElements = form.querySelectorAll(':invalid');
invalidElements.forEach(el => {
console.log(`${el.name}: ${el.validationMessage}`);
});
// Get all valid elements
const validElements = form.querySelectorAll(':valid');
setCustomValidity()
const form = document.getElementById('registration-form');
const studentIdInput = form.elements.studentId;
const emailInput = form.elements.email;
// Set custom validity message
studentIdInput.addEventListener('input', () => {
const value = studentIdInput.value;
if (value.length === 0) {
studentIdInput.setCustomValidity('StudentID is required');
} else if (!/^\d{10}$/.test(value)) {
studentIdInput.setCustomValidity('StudentID must be 10 digits');
} else if (!value.startsWith('20')) {
studentIdInput.setCustomValidity('StudentID must start with the enrollment year');
} else {
studentIdInput.setCustomValidity(''); // Reset = valid
}
updateFieldStatus(studentIdInput);
});
emailInput.addEventListener('input', () => {
const value = emailInput.value;
if (value.length === 0) {
emailInput.setCustomValidity('Email is required');
} else if (!value.includes('@')) {
emailInput.setCustomValidity('Email must contain @');
} else if (!value.endsWith('@university.edu')) {
emailInput.setCustomValidity('Must use a university email (@university.edu)');
} else {
emailInput.setCustomValidity('');
}
updateFieldStatus(emailInput);
});
function updateFieldStatus(input) {
const isValid = input.checkValidity();
const feedback = input.nextElementSibling;
input.classList.remove('is-valid', 'is-invalid');
input.classList.add(isValid ? 'is-valid' : 'is-invalid');
if (feedback && feedback.classList.contains('feedback')) {
feedback.textContent = isValid ? '' : input.validationMessage;
}
}
// Submit handler
form.addEventListener('submit', (event) => {
event.preventDefault();
if (!form.checkValidity()) {
// Show all errors
form.querySelectorAll(':invalid').forEach(input => {
updateFieldStatus(input);
});
return;
}
// Form is valid, submit
submitForm(new FormData(form));
});
Real-World Example: Graduation Registration Form
class GraduationRegistration {
constructor() {
this.form = document.getElementById('graduation-form');
this.submitting = false;
this.init();
}
init() {
// Catch the submit
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.handleSubmit();
});
// Live validation
this.form.querySelectorAll('input, select, textarea').forEach(input => {
input.addEventListener('input', () => {
this.validateField(input);
this.updateSubmitButton();
});
});
// File upload handling
this.form.querySelectorAll('input[type="file"]').forEach(input => {
input.addEventListener('change', (e) => {
this.handleFileUpload(e.target);
});
});
}
validateField(field) {
const value = field.value.trim();
let isValid = true;
let message = '';
switch (field.name) {
case 'name':
if (value.length < 3) {
isValid = false;
message = 'Name at least 3 characters';
}
break;
case 'studentId':
if (!/^\d{10}$/.test(value)) {
isValid = false;
message = 'StudentID must be 10 digits';
}
break;
case 'email':
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
isValid = false;
message = 'Invalid email format';
}
break;
case 'gpa':
const gpa = parseFloat(value);
if (isNaN(gpa) || gpa < 0 || gpa > 4) {
isValid = false;
message = 'GPA must be between 0.00 - 4.00';
} else if (gpa < 2.5) {
isValid = false;
message = 'GPA must be at least 2.50 for graduation';
}
break;
case 'semester':
const sem = parseInt(value);
if (isNaN(sem) || sem < 6 || sem > 14) {
isValid = false;
message = 'Semester must be between 6 - 14';
}
break;
case 'thesis_title':
if (value.length < 10) {
isValid = false;
message = 'Thesis title at least 10 characters';
} else if (value.length > 200) {
isValid = false;
message = 'Thesis title at most 200 characters';
}
break;
}
field.setCustomValidity(isValid ? '' : message);
this.updateFieldUI(field, isValid, message);
return isValid;
}
updateFieldUI(field, isValid, message) {
const feedbackEl = field.parentElement.querySelector('.feedback');
field.classList.remove('is-valid', 'is-invalid');
field.classList.add(isValid ? 'is-valid' : 'is-invalid');
if (feedbackEl) {
feedbackEl.textContent = isValid ? '' : message;
feedbackEl.classList.toggle('error', !isValid);
}
}
handleFileUpload(input) {
const file = input.files[0];
if (!file) return;
const maxSize = {
'transcript': 2 * 1024 * 1024, // 2MB
'thesis': 5 * 1024 * 1024, // 5MB
'ktp': 1 * 1024 * 1024, // 1MB
'passportPhoto': 500 * 1024 // 500KB
};
const allowedTypes = {
'transcript': ['application/pdf'],
'thesis': ['application/pdf'],
'ktp': ['image/jpeg', 'image/png'],
'passportPhoto': ['image/jpeg', 'image/png']
};
const fieldName = input.name;
const fileMaxSize = maxSize[fieldName] || 2 * 1024 * 1024;
const fileAllowedTypes = allowedTypes[fieldName] || ['application/pdf'];
let isValid = true;
let message = '';
if (file.size > fileMaxSize) {
isValid = false;
message = `Maximum file size ${this.formatSize(fileMaxSize)}`;
} else if (!fileAllowedTypes.includes(file.type)) {
isValid = false;
message = `File format must be: ${fileAllowedTypes.join(', ')}`;
}
if (isValid) {
this.showFilePreview(input, file);
} else {
this.showFileError(input, message);
input.value = '';
}
}
showFilePreview(input, file) {
const container = input.parentElement;
let preview = container.querySelector('.file-preview');
if (!preview) {
preview = document.createElement('div');
preview.className = 'file-preview';
container.appendChild(preview);
}
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = (e) => {
preview.innerHTML = `
<img src="${e.target.result}" alt="Preview" style="max-width:200px">
<p>${file.name} (${this.formatSize(file.size)})</p>
<button type="button" class="remove-file" data-field="${input.name}">Delete</button>
`;
};
reader.readAsDataURL(file);
} else {
preview.innerHTML = `
<p>📄 ${file.name} (${this.formatSize(file.size)})</p>
<button type="button" class="remove-file" data-field="${input.name}">Delete</button>
`;
}
}
showFileError(input, message) {
const container = input.parentElement;
let error = container.querySelector('.file-error');
if (!error) {
error = document.createElement('p');
error.className = 'file-error error';
container.appendChild(error);
}
error.textContent = message;
}
formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
updateSubmitButton() {
const submitBtn = document.getElementById('submit-btn');
const allValid = this.form.checkValidity();
submitBtn.disabled = !allValid || this.submitting;
}
async handleSubmit() {
if (this.submitting) return;
// Validate all fields
let allValid = true;
this.form.querySelectorAll('[required]').forEach(field => {
if (!this.validateField(field)) {
allValid = false;
}
});
if (!allValid) {
this.showError('Please fill in all fields correctly');
return;
}
this.submitting = true;
this.updateSubmitButton();
try {
const formData = new FormData(this.form);
// Send to the server
const response = await fetch('/api/graduation/register', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
this.showSuccess('Graduation registration successful! Please check your email for confirmation');
this.form.reset();
document.querySelectorAll('.file-preview').forEach(el => el.remove());
} else {
this.showError(result.message || 'Failed to register for graduation');
}
} catch (error) {
console.error('Error:', error);
this.showError('A connection error occurred. Please try again.');
} finally {
this.submitting = false;
this.updateSubmitButton();
}
}
showError(message) {
const container = document.getElementById('form-messages');
container.innerHTML = `<div class="alert alert-error">${message}</div>`;
}
showSuccess(message) {
const container = document.getElementById('form-messages');
container.innerHTML = `<div class="alert alert-success">${message}</div>`;
}
}
// Initialize
new GraduationRegistration();
Example: Multi-Step Form with Validation
class MultiStepForm {
constructor(formId) {
this.form = document.getElementById(formId);
this.currentStep = 0;
this.steps = this.form.querySelectorAll('.form-step');
this.totalSteps = this.steps.length;
this.data = {};
this.init();
}
init() {
// Navigation buttons
this.form.querySelectorAll('.next-btn').forEach(btn => {
btn.addEventListener('click', () => this.nextStep());
});
this.form.querySelectorAll('.prev-btn').forEach(btn => {
btn.addEventListener('click', () => this.prevStep());
});
// Submit
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.submitAll();
});
// Show first step
this.steps[0].classList.add('active');
this.updateProgress();
}
nextStep() {
if (!this.validateCurrentStep()) return;
this.saveCurrentStepData();
this.steps[this.currentStep].classList.remove('active');
this.currentStep++;
this.steps[this.currentStep].classList.add('active');
this.updateProgress();
}
prevStep() {
this.saveCurrentStepData();
this.steps[this.currentStep].classList.remove('active');
this.currentStep--;
this.steps[this.currentStep].classList.add('active');
this.updateProgress();
}
validateCurrentStep() {
const step = this.steps[this.currentStep];
const inputs = step.querySelectorAll('input[required], select[required], textarea[required]');
let valid = true;
inputs.forEach(input => {
if (!input.value.trim()) {
input.classList.add('is-invalid');
valid = false;
} else {
input.classList.remove('is-invalid');
}
if (!input.checkValidity()) {
valid = false;
}
});
if (!valid) {
this.showError('Please fill in all required fields');
}
return valid;
}
saveCurrentStepData() {
const step = this.steps[this.currentStep];
const formData = new FormData(step.querySelector('form') || this.form);
for (const [key, value] of formData) {
this.data[key] = value;
}
}
updateProgress() {
const progress = ((this.currentStep + 1) / this.totalSteps) * 100;
const progressBar = this.form.querySelector('.progress-fill');
progressBar.style.width = `${progress}%`;
// Update step indicators
this.form.querySelectorAll('.step-indicator').forEach((ind, i) => {
ind.classList.toggle('active', i === this.currentStep);
ind.classList.toggle('completed', i < this.currentStep);
});
}
async submitAll() {
const allData = { ...this.data };
try {
const response = await fetch('/api/multi-step-submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(allData)
});
const result = await response.json();
if (result.success) {
this.showSuccess('Form successful disubmit!');
}
} catch (error) {
this.showError('Failed to send data');
}
}
showError(message) {
const msg = this.form.querySelector('.form-message');
msg.className = 'form-message error';
msg.textContent = message;
msg.style.display = 'block';
}
showSuccess(message) {
const msg = this.form.querySelector('.form-message');
msg.className = 'form-message success';
msg.textContent = message;
msg.style.display = 'block';
}
}
// Usage
new MultiStepForm('registration-form');
Example: Lecturer Evaluation Form
class FacultyEvaluation {
constructor() {
this.form = document.getElementById('eval-form');
this.ratings = {};
this.init();
}
init() {
// Catch all star ratings
this.form.querySelectorAll('.star-rating').forEach(rating => {
const stars = rating.querySelectorAll('.star');
const fieldName = rating.dataset.field;
stars.forEach((star, index) => {
star.addEventListener('click', () => {
this.setRating(fieldName, index + 1);
this.updateStars(rating, index + 1);
});
star.addEventListener('mouseenter', () => {
this.previewStars(rating, index + 1);
});
});
rating.addEventListener('mouseleave', () => {
this.resetStars(rating, this.ratings[fieldName] || 0);
});
});
// Catch the feedback textareas
this.form.querySelectorAll('textarea').forEach(textarea => {
textarea.addEventListener('input', () => {
this.updateCharCount(textarea);
});
});
// Submit
this.form.addEventListener('submit', (e) => {
e.preventDefault();
this.submitEvaluation();
});
}
setRating(field, value) {
this.ratings[field] = value;
this.updateSubmitButton();
}
updateStars(container, count) {
container.querySelectorAll('.star').forEach((star, i) => {
star.classList.toggle('active', i < count);
});
}
previewStars(container, count) {
container.querySelectorAll('.star').forEach((star, i) => {
star.classList.toggle('hover', i < count);
});
}
resetStars(container, count) {
container.querySelectorAll('.star').forEach((star, i) => {
star.classList.remove('hover');
star.classList.toggle('active', i < count);
});
}
updateCharCount(textarea) {
const counter = textarea.parentElement.querySelector('.char-count');
const remaining = 500 - textarea.value.length;
counter.textContent = `${textarea.value.length}/500`;
counter.classList.toggle('warning', remaining < 50);
counter.classList.toggle('danger', remaining < 0);
}
updateSubmitButton() {
const requiredRatings = ['clarity', 'mastery', 'responsive', 'attendance'];
const allRated = requiredRatings.every(field => this.ratings[field]);
this.form.querySelector('.submit-btn').disabled = !allRated;
}
async submitEvaluation() {
const formData = {
lecturerId: this.form.querySelector('[name="lecturer_id"]').value,
course: this.form.querySelector('[name="course"]').value,
ratings: this.ratings,
feedback: {
strengths: this.form.querySelector('[name="strengths"]').value,
kesubtractan: this.form.querySelector('[name="kesubtractan"]').value,
saran: this.form.querySelector('[name="saran"]').value
},
timestamp: new Date().toISOString()
};
try {
const response = await fetch('/api/evaluation/submit', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
this.showSuccess('Evaluation sent successfully! Thank you');
this.form.reset();
this.ratings = {};
this.form.querySelectorAll('.star').forEach(s => s.classList.remove('active'));
}
} catch (error) {
this.showError('Failed to send evaluation');
}
}
}
Example: Dynamic Form Builder
class FormBuilder {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.fields = [];
this.formData = {};
}
addField(config) {
const field = {
id: config.id || `field-${this.fields.length}`,
type: config.type || 'text',
label: config.label || '',
name: config.name || '',
required: config.required || false,
placeholder: config.placeholder || '',
options: config.options || [],
validators: config.validators || [],
...config
};
this.fields.push(field);
this.renderField(field);
return this;
}
renderField(field) {
const wrapper = document.createElement('div');
wrapper.className = 'form-field';
wrapper.id = `wrapper-${field.id}`;
let inputHTML = '';
switch (field.type) {
case 'text':
case 'email':
case 'number':
case 'tel':
case 'date':
inputHTML = `
<input
type="${field.type}"
id="${field.id}"
name="${field.name}"
placeholder="${field.placeholder}"
${field.required ? 'required' : ''}
class="form-control"
>
`;
break;
case 'textarea':
inputHTML = `
<textarea
id="${field.id}"
name="${field.name}"
placeholder="${field.placeholder}"
${field.required ? 'required' : ''}
class="form-control"
rows="4"
></textarea>
`;
break;
case 'select':
inputHTML = `
<select id="${field.id}" name="${field.name}" class="form-control" ${field.required ? 'required' : ''}>
<option value="">${field.placeholder || 'Select...'}</option>
${field.options.map(opt => `
<option value="${opt.value}">${opt.label}</option>
`).join('')}
</select>
`;
break;
case 'radio':
inputHTML = field.options.map(opt => `
<label class="radio-label">
<input type="radio" name="${field.name}" value="${opt.value}" ${field.required ? 'required' : ''}>
${opt.label}
</label>
`).join('');
break;
case 'checkbox':
inputHTML = field.options.map(opt => `
<label class="checkbox-label">
<input type="checkbox" name="${field.name}" value="${opt.value}">
${opt.label}
</label>
`).join('');
break;
case 'file':
inputHTML = `
<input type="file" id="${field.id}" name="${field.name}"
${field.accept ? `accept="${field.accept}"` : ''}
${field.multiple ? 'multiple' : ''}
${field.required ? 'required' : ''}>
`;
break;
}
wrapper.innerHTML = `
<label for="${field.id}" class="form-label">
${field.label}
${field.required ? '<span class="required">*</span>' : ''}
</label>
${inputHTML}
<div class="feedback" id="feedback-${field.id}"></div>
`;
this.container.appendChild(wrapper);
// Add validation
const input = wrapper.querySelector('input, select, textarea');
if (input) {
input.addEventListener('input', () => {
this.validateField(field, input.value);
});
}
}
validateField(field, value) {
let isValid = true;
let message = '';
for (const validator of field.validators) {
const result = validator(value);
if (!result.valid) {
isValid = false;
message = result.message;
break;
}
}
const feedback = document.getElementById(`feedback-${field.id}`);
const input = document.getElementById(field.id);
input.classList.toggle('is-valid', isValid && value);
input.classList.toggle('is-invalid', !isValid);
feedback.textContent = !isValid ? message : '';
return isValid;
}
getData() {
const data = {};
const formData = new FormData();
this.fields.forEach(field => {
const input = document.getElementById(field.id);
if (input) {
if (field.type === 'checkbox') {
const checked = document.querySelectorAll(`input[name="${field.name}"]:checked`);
data[field.name] = Array.from(checked).map(cb => cb.value);
} else {
data[field.name] = input.value;
}
}
});
return data;
}
validateAll() {
let allValid = true;
this.fields.forEach(field => {
if (field.required) {
const input = document.getElementById(field.id);
const value = input ? input.value : '';
if (!value.trim()) {
allValid = false;
this.validateField(field, '');
}
}
});
return allValid;
}
}
// Usage
const builder = new FormBuilder('dynamic-form');
builder
.addField({
type: 'text',
label: 'Full name',
name: 'name',
required: true,
placeholder: 'Enter your full name',
validators: [
(v) => ({ valid: v.length >= 3, message: 'Name at least 3 characters' })
]
})
.addField({
type: 'email',
label: 'Email',
name: 'email',
required: true,
placeholder: '[email protected]',
validators: [
(v) => ({ valid: v.includes('@'), message: 'Invalid email' })
]
})
.addField({
type: 'select',
label: 'Major',
name: 'majorCode',
required: true,
options: [
{ value: 'tif', label: 'Computer Science' },
{ value: 'si', label: 'Information Systems' },
{ value: 'ce', label: 'Computer Engineering' }
]
})
.addField({
type: 'textarea',
label: 'Motivasi',
name: 'motivasi',
required: true,
placeholder: 'Tell us about your motivation...',
validators: [
(v) => ({ valid: v.length >= 50, message: 'At least 50 characters' })
]
});
Code Explanation
FormData Methods
formData.get(name)- Get the value of the first field with a given nameformData.getAll(name)- Get all values (for checkboxes/radios)formData.set(name, value)- Set/replace a field's valueformData.append(name, value)- Add a new fieldformData.delete(name)- Remove a fieldformData.has(name)- Check whether a field existsformData.entries()- Iterator over all key-value pairs
Validation
form.checkValidity()- Check the validity of the entire forminput.checkValidity()- Check the validity of a single fieldinput.setCustomValidity(message)- Set a custom error messageinput.validationMessage- The validation message displayed by the browser- CSS pseudo-classes
:validand:invalidfor styling
Submit Methods
form.submit()- Submit without validation and without the eventform.requestSubmit()- Submit with validation and triggers the submit eventevent.preventDefault()- Prevent the default submit in the event handler
Conclusion
The Forms API provides comprehensive tools for handling forms in JavaScript. By mastering FormData, validation, and custom validation, you can build robust form handling for a campus application. For example, in a graduation registration form, combining the browser's built-in validation with custom validation ensures that incoming data is valid and complete. Always use preventDefault() in your submit handler to control the submission process, and leverage FormData to submit data that includes file uploads.