javascript
exercises
exercises.js⚡javascript
/**
* ========================================
* 10.5 Forms and User Input - Exercises
* ========================================
*
* Practice form handling, validation, and user input processing.
* Complete each exercise by filling in the code.
*/
/**
* EXERCISE 1: Form Value Reader
*
* Create a function that extracts all values from a form
* and returns them as an object.
*
* Requirements:
* - Handle text, email, password inputs
* - Handle checkboxes (return array of checked values for same-name checkboxes)
* - Handle radio buttons (return selected value)
* - Handle select elements (single and multiple)
* - Handle textarea
*/
function getFormValues(form) {
// Your code here
}
// Test HTML for Exercise 1:
/*
<form id="testForm">
<input type="text" name="username" value="john_doe">
<input type="email" name="email" value="john@example.com">
<input type="checkbox" name="skills" value="js" checked>
<input type="checkbox" name="skills" value="python" checked>
<input type="checkbox" name="skills" value="java">
<input type="radio" name="level" value="junior">
<input type="radio" name="level" value="senior" checked>
<select name="country">
<option value="us" selected>USA</option>
<option value="uk">UK</option>
</select>
<textarea name="bio">Hello world</textarea>
</form>
*/
// Expected output:
// {
// username: "john_doe",
// email: "john@example.com",
// skills: ["js", "python"],
// level: "senior",
// country: "us",
// bio: "Hello world"
// }
/**
* EXERCISE 2: Email Validator
*
* Create a function that validates an email input field
* and returns an object with validation details.
*/
function validateEmail(inputElement) {
// Your code here
// Return: { valid: boolean, errors: string[] }
}
// Example usage:
// const emailInput = document.querySelector('#email');
// emailInput.value = "invalid-email";
// validateEmail(emailInput);
// → { valid: false, errors: ["Invalid email format"] }
/**
* EXERCISE 3: Password Strength Checker
*
* Create a function that checks password strength and returns
* a score from 0-5 with feedback.
*
* Criteria (1 point each):
* - At least 8 characters
* - Contains lowercase letter
* - Contains uppercase letter
* - Contains number
* - Contains special character (!@#$%^&*)
*/
function checkPasswordStrength(password) {
// Your code here
// Return: { score: number, strength: string, feedback: string[] }
}
// Example:
// checkPasswordStrength("Pass123!")
// → { score: 5, strength: "strong", feedback: [] }
//
// checkPasswordStrength("abc")
// → { score: 1, strength: "weak", feedback: ["Add uppercase", "Add number", "Add special character", "Make it 8+ chars"] }
/**
* EXERCISE 4: Form Validator
*
* Create a function that validates a complete form based on
* validation rules object.
*/
function validateForm(form, rules) {
// Your code here
// Return: { valid: boolean, errors: { fieldName: string[] } }
}
// Example rules object:
const validationRules = {
username: {
required: true,
minLength: 3,
maxLength: 20,
pattern: /^[a-zA-Z0-9_]+$/,
},
email: {
required: true,
type: 'email',
},
age: {
required: false,
type: 'number',
min: 18,
max: 120,
},
password: {
required: true,
minLength: 8,
},
};
// Expected output:
// {
// valid: false,
// errors: {
// username: ["Username must be at least 3 characters"],
// password: ["Password is required"]
// }
// }
/**
* EXERCISE 5: Live Search Input
*
* Create a function that sets up a search input with debouncing.
* It should call the callback only after the user stops typing.
*/
function setupLiveSearch(inputElement, callback, delay = 300) {
// Your code here
// Should call callback(searchQuery) after user stops typing
}
// Example usage:
// const searchInput = document.querySelector('#search');
// setupLiveSearch(searchInput, (query) => {
// console.log('Searching for:', query);
// }, 500);
/**
* EXERCISE 6: Character Counter
*
* Create a function that adds a live character counter
* to a textarea element.
*/
function addCharacterCounter(textarea, maxLength) {
// Your code here
// Should create a counter element below the textarea
// Update counter on input: "45 / 280 characters"
// Add warning style when near limit (90%+)
// Prevent typing when limit reached
}
// Example usage:
// addCharacterCounter(document.querySelector('#tweet'), 280);
/**
* EXERCISE 7: Form Data Serializer
*
* Create a function that converts FormData to different formats.
*/
function serializeForm(form, format = 'object') {
// Your code here
// format: 'object' | 'json' | 'urlencoded' | 'formdata'
}
// Examples:
// serializeForm(form, 'object')
// → { name: "John", email: "john@example.com" }
//
// serializeForm(form, 'json')
// → '{"name":"John","email":"john@example.com"}'
//
// serializeForm(form, 'urlencoded')
// → "name=John&email=john%40example.com"
//
// serializeForm(form, 'formdata')
// → FormData object
/**
* EXERCISE 8: Input Mask
*
* Create a function that applies an input mask to a text field.
* Mask format: # = digit, A = letter, * = any character
*/
function applyInputMask(input, mask) {
// Your code here
// mask examples:
// Phone: "(###) ###-####"
// Date: "##/##/####"
// Credit card: "#### #### #### ####"
}
// Example usage:
// applyInputMask(phoneInput, "(###) ###-####");
// User types "1234567890"
// Display shows "(123) 456-7890"
/**
* EXERCISE 9: Auto-save Form
*
* Create a function that auto-saves form data to localStorage
* and restores it on page load.
*/
function setupAutoSave(form, storageKey, interval = 5000) {
// Your code here
// Should:
// - Save form data to localStorage every `interval` ms
// - Save on any input change (debounced)
// - Restore form data on load
// - Clear saved data on successful submit
// - Return methods: { save(), restore(), clear() }
}
// Example usage:
// const autosave = setupAutoSave(
// document.querySelector('#draftForm'),
// 'draft-post',
// 3000
// );
// autosave.save(); // Manual save
// autosave.restore(); // Manual restore
// autosave.clear(); // Clear saved data
/**
* EXERCISE 10: Multi-step Form
*
* Create a class that manages a multi-step form wizard.
*/
class FormWizard {
constructor(formElement, steps) {
// steps: array of step configurations
// { id: string, validate: function }
// Your code here
}
getCurrentStep() {
// Return current step index
}
nextStep() {
// Validate current step and move to next
// Return: { success: boolean, errors?: string[] }
}
prevStep() {
// Move to previous step
}
goToStep(index) {
// Go to specific step (if allowed)
}
submit() {
// Validate all steps and submit
}
}
// Example usage:
/*
const wizard = new FormWizard(document.querySelector('#multiStepForm'), [
{
id: 'personal-info',
validate: (data) => data.name && data.email
},
{
id: 'address',
validate: (data) => data.street && data.city
},
{
id: 'payment',
validate: (data) => data.cardNumber
}
]);
wizard.nextStep(); // Validates step 1, moves to step 2
wizard.prevStep(); // Moves back to step 1
wizard.submit(); // Submits if all valid
*/
/**
* EXERCISE 11: Dependent Dropdowns
*
* Create a function that sets up cascading/dependent select dropdowns.
*/
function setupDependentDropdowns(config) {
// Your code here
// config: { parentSelect, childSelect, options }
// options: { parentValue: [childOptions] }
}
// Example:
// setupDependentDropdowns({
// parentSelect: document.querySelector('#country'),
// childSelect: document.querySelector('#city'),
// options: {
// 'us': [
// { value: 'nyc', label: 'New York' },
// { value: 'la', label: 'Los Angeles' }
// ],
// 'uk': [
// { value: 'london', label: 'London' },
// { value: 'manchester', label: 'Manchester' }
// ]
// }
// });
/**
* EXERCISE 12: File Upload with Preview
*
* Create a function that sets up file upload with preview and validation.
*/
function setupFileUpload(inputElement, options = {}) {
// options: {
// maxSize: number (bytes),
// allowedTypes: string[],
// maxFiles: number,
// previewContainer: Element,
// onValidationError: function,
// onFilesSelected: function
// }
// Your code here
// Should:
// - Validate file size and type
// - Show image previews
// - Show file info for non-images
// - Allow removing selected files
}
// Example usage:
// setupFileUpload(document.querySelector('#imageUpload'), {
// maxSize: 5 * 1024 * 1024, // 5MB
// allowedTypes: ['image/jpeg', 'image/png', 'image/gif'],
// maxFiles: 5,
// previewContainer: document.querySelector('#previews'),
// onValidationError: (errors) => console.log(errors),
// onFilesSelected: (files) => console.log('Selected:', files)
// });
/**
* EXERCISE 13: Form Field Array
*
* Create a function that manages dynamic array fields
* (add/remove/reorder items).
*/
function createFieldArray(containerElement, fieldTemplate, options = {}) {
// options: {
// minItems: number,
// maxItems: number,
// onAdd: function,
// onRemove: function,
// sortable: boolean
// }
// Your code here
// Return: { add(), remove(index), getValues(), setValues(array) }
}
// Example:
// const phones = createFieldArray(
// document.querySelector('#phoneNumbers'),
// '<input type="tel" name="phones[]" placeholder="Phone number">',
// { minItems: 1, maxItems: 5 }
// );
//
// phones.add(); // Add new phone field
// phones.remove(1); // Remove second phone field
// phones.getValues(); // Get all phone values as array
// phones.setValues(['+1234567890', '+0987654321']); // Set values
/**
* EXERCISE 14: Credit Card Form
*
* Create a credit card input component with:
* - Card number formatting (4 groups of 4)
* - Expiry date formatting (MM/YY)
* - CVV input (3-4 digits)
* - Card type detection (Visa, Mastercard, Amex)
* - Luhn algorithm validation
*/
class CreditCardForm {
constructor(formElement) {
// Your code here
}
detectCardType(number) {
// Return: 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown'
}
validateCardNumber(number) {
// Use Luhn algorithm
// Return: boolean
}
validateExpiry(expiry) {
// Check if date is in future
// Return: boolean
}
formatCardNumber(number) {
// Add spaces every 4 digits
// Return: formatted string
}
getCardData() {
// Return: { number, expiry, cvv, type, valid }
}
}
/**
* EXERCISE 15: Form Submission Handler
*
* Create a robust form submission handler with:
* - Loading state
* - Error handling
* - Retry logic
* - Success/error callbacks
*/
function createFormSubmitHandler(form, options) {
// options: {
// url: string,
// method: 'POST' | 'PUT',
// headers: object,
// transformData: function,
// validate: function,
// onSubmitStart: function,
// onSuccess: function,
// onError: function,
// onComplete: function,
// retries: number,
// retryDelay: number
// }
// Your code here
// Return: { submit(), abort() }
}
// Example usage:
/*
const handler = createFormSubmitHandler(
document.querySelector('#contactForm'),
{
url: '/api/contact',
method: 'POST',
validate: (data) => {
if (!data.email) return { valid: false, error: 'Email required' };
return { valid: true };
},
transformData: (formData) => JSON.stringify(Object.fromEntries(formData)),
headers: { 'Content-Type': 'application/json' },
onSubmitStart: () => showSpinner(),
onSuccess: (response) => showMessage('Sent!'),
onError: (error) => showError(error.message),
onComplete: () => hideSpinner(),
retries: 3,
retryDelay: 1000
}
);
// handler.submit(); // Submit form
// handler.abort(); // Cancel ongoing submission
*/
// ============================================
// SOLUTIONS (Hidden - Scroll to reveal)
// ============================================
/*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* SOLUTIONS BELOW
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
// SOLUTION 1: Form Value Reader
function getFormValuesSolution(form) {
const result = {};
const formData = new FormData(form);
// Group values by name
const valueGroups = {};
for (const [name, value] of formData.entries()) {
if (!valueGroups[name]) {
valueGroups[name] = [];
}
valueGroups[name].push(value);
}
// Process each field
for (const [name, values] of Object.entries(valueGroups)) {
const element = form.elements[name];
// Check if it's a multi-value field (checkboxes or multi-select)
if (element instanceof RadioNodeList && element[0]?.type === 'checkbox') {
result[name] = values;
} else if (element instanceof HTMLSelectElement && element.multiple) {
result[name] = values;
} else {
result[name] = values.length === 1 ? values[0] : values;
}
}
return result;
}
// SOLUTION 2: Email Validator
function validateEmailSolution(inputElement) {
const errors = [];
const value = inputElement.value.trim();
if (inputElement.required && !value) {
errors.push('Email is required');
}
if (value && inputElement.validity.typeMismatch) {
errors.push('Invalid email format');
}
// Additional custom validation
if (value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
if (!errors.includes('Invalid email format')) {
errors.push('Invalid email format');
}
}
// Check for common issues
if (value.includes('..')) {
errors.push('Email cannot contain consecutive dots');
}
}
return {
valid: errors.length === 0,
errors,
};
}
// SOLUTION 3: Password Strength Checker
function checkPasswordStrengthSolution(password) {
const feedback = [];
let score = 0;
// Check length
if (password.length >= 8) {
score++;
} else {
feedback.push('Make it 8+ characters');
}
// Check lowercase
if (/[a-z]/.test(password)) {
score++;
} else {
feedback.push('Add lowercase letter');
}
// Check uppercase
if (/[A-Z]/.test(password)) {
score++;
} else {
feedback.push('Add uppercase letter');
}
// Check number
if (/[0-9]/.test(password)) {
score++;
} else {
feedback.push('Add number');
}
// Check special character
if (/[!@#$%^&*]/.test(password)) {
score++;
} else {
feedback.push('Add special character (!@#$%^&*)');
}
// Determine strength label
let strength;
if (score <= 1) strength = 'very weak';
else if (score === 2) strength = 'weak';
else if (score === 3) strength = 'fair';
else if (score === 4) strength = 'strong';
else strength = 'very strong';
return { score, strength, feedback };
}
// SOLUTION 4: Form Validator
function validateFormSolution(form, rules) {
const errors = {};
let valid = true;
for (const [fieldName, fieldRules] of Object.entries(rules)) {
const element = form.elements[fieldName];
const value = element?.value?.trim() ?? '';
const fieldErrors = [];
// Required check
if (fieldRules.required && !value) {
fieldErrors.push(`${fieldName} is required`);
}
if (value) {
// Min length
if (fieldRules.minLength && value.length < fieldRules.minLength) {
fieldErrors.push(
`${fieldName} must be at least ${fieldRules.minLength} characters`
);
}
// Max length
if (fieldRules.maxLength && value.length > fieldRules.maxLength) {
fieldErrors.push(
`${fieldName} must be at most ${fieldRules.maxLength} characters`
);
}
// Pattern
if (fieldRules.pattern && !fieldRules.pattern.test(value)) {
fieldErrors.push(`${fieldName} format is invalid`);
}
// Type validation
if (fieldRules.type === 'email') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
fieldErrors.push(`${fieldName} must be a valid email`);
}
}
if (fieldRules.type === 'number') {
const num = Number(value);
if (isNaN(num)) {
fieldErrors.push(`${fieldName} must be a number`);
} else {
if (fieldRules.min !== undefined && num < fieldRules.min) {
fieldErrors.push(`${fieldName} must be at least ${fieldRules.min}`);
}
if (fieldRules.max !== undefined && num > fieldRules.max) {
fieldErrors.push(`${fieldName} must be at most ${fieldRules.max}`);
}
}
}
}
if (fieldErrors.length > 0) {
errors[fieldName] = fieldErrors;
valid = false;
}
}
return { valid, errors };
}
// SOLUTION 5: Live Search Input
function setupLiveSearchSolution(inputElement, callback, delay = 300) {
let timeoutId = null;
inputElement.addEventListener('input', function (e) {
const query = e.target.value.trim();
// Clear previous timeout
if (timeoutId) {
clearTimeout(timeoutId);
}
// Set new timeout
timeoutId = setTimeout(() => {
callback(query);
}, delay);
});
// Return cleanup function
return function cleanup() {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}
// SOLUTION 6: Character Counter
function addCharacterCounterSolution(textarea, maxLength) {
// Create counter element
const counter = document.createElement('div');
counter.className = 'char-counter';
counter.style.cssText = 'font-size: 12px; color: #666; margin-top: 4px;';
textarea.parentNode.insertBefore(counter, textarea.nextSibling);
function updateCounter() {
const currentLength = textarea.value.length;
const remaining = maxLength - currentLength;
counter.textContent = `${currentLength} / ${maxLength} characters`;
// Warning when near limit (90%+)
if (currentLength >= maxLength * 0.9) {
counter.style.color = '#e74c3c';
counter.style.fontWeight = 'bold';
} else {
counter.style.color = '#666';
counter.style.fontWeight = 'normal';
}
}
// Prevent exceeding limit
textarea.addEventListener('input', function () {
if (this.value.length > maxLength) {
this.value = this.value.substring(0, maxLength);
}
updateCounter();
});
// Also handle paste
textarea.addEventListener('paste', function () {
setTimeout(() => {
if (this.value.length > maxLength) {
this.value = this.value.substring(0, maxLength);
}
updateCounter();
}, 0);
});
// Initial update
updateCounter();
return counter;
}
// SOLUTION 7: Form Data Serializer
function serializeFormSolution(form, format = 'object') {
const formData = new FormData(form);
switch (format) {
case 'formdata':
return formData;
case 'object': {
const obj = {};
for (const [key, value] of formData.entries()) {
if (obj[key]) {
// Handle multiple values for same key
if (Array.isArray(obj[key])) {
obj[key].push(value);
} else {
obj[key] = [obj[key], value];
}
} else {
obj[key] = value;
}
}
return obj;
}
case 'json':
return JSON.stringify(serializeFormSolution(form, 'object'));
case 'urlencoded': {
const params = new URLSearchParams();
for (const [key, value] of formData.entries()) {
params.append(key, value);
}
return params.toString();
}
default:
throw new Error(`Unknown format: ${format}`);
}
}
// SOLUTION 8: Input Mask
function applyInputMaskSolution(input, mask) {
function formatValue(value) {
// Remove all non-alphanumeric
const clean = value.replace(/[^a-zA-Z0-9]/g, '');
let result = '';
let valueIndex = 0;
for (let i = 0; i < mask.length && valueIndex < clean.length; i++) {
const maskChar = mask[i];
if (maskChar === '#') {
// Digit only
if (/\d/.test(clean[valueIndex])) {
result += clean[valueIndex];
valueIndex++;
} else {
valueIndex++;
i--; // Try again with next char
}
} else if (maskChar === 'A') {
// Letter only
if (/[a-zA-Z]/.test(clean[valueIndex])) {
result += clean[valueIndex];
valueIndex++;
} else {
valueIndex++;
i--;
}
} else if (maskChar === '*') {
// Any character
result += clean[valueIndex];
valueIndex++;
} else {
// Literal character from mask
result += maskChar;
}
}
return result;
}
input.addEventListener('input', function (e) {
const cursorPos = this.selectionStart;
const oldLength = this.value.length;
this.value = formatValue(this.value);
const newLength = this.value.length;
const newCursorPos = cursorPos + (newLength - oldLength);
this.setSelectionRange(newCursorPos, newCursorPos);
});
input.addEventListener('keydown', function (e) {
// Allow navigation and editing keys
const allowedKeys = [
'Backspace',
'Delete',
'ArrowLeft',
'ArrowRight',
'Tab',
];
if (allowedKeys.includes(e.key)) {
return;
}
// Prevent if max length reached
const clean = this.value.replace(/[^a-zA-Z0-9]/g, '');
const maxLength = mask.replace(/[^#A*]/g, '').length;
if (
clean.length >= maxLength &&
this.selectionStart === this.selectionEnd
) {
e.preventDefault();
}
});
}
// SOLUTION 9: Auto-save Form
function setupAutoSaveSolution(form, storageKey, interval = 5000) {
let intervalId = null;
let debounceId = null;
function save() {
const formData = new FormData(form);
const data = {};
for (const [key, value] of formData.entries()) {
if (data[key]) {
if (Array.isArray(data[key])) {
data[key].push(value);
} else {
data[key] = [data[key], value];
}
} else {
data[key] = value;
}
}
localStorage.setItem(storageKey, JSON.stringify(data));
console.log('Form auto-saved');
}
function restore() {
const saved = localStorage.getItem(storageKey);
if (!saved) return false;
try {
const data = JSON.parse(saved);
for (const [name, value] of Object.entries(data)) {
const elements = form.elements[name];
if (!elements) continue;
if (elements instanceof RadioNodeList) {
// Handle radio/checkbox groups
for (const element of elements) {
if (element.type === 'checkbox') {
element.checked = Array.isArray(value)
? value.includes(element.value)
: value === element.value;
} else if (element.type === 'radio') {
element.checked = element.value === value;
}
}
} else if (elements.type === 'checkbox') {
elements.checked = Boolean(value);
} else {
elements.value = value;
}
}
console.log('Form restored from auto-save');
return true;
} catch (e) {
console.error('Failed to restore form:', e);
return false;
}
}
function clear() {
localStorage.removeItem(storageKey);
console.log('Auto-save cleared');
}
// Set up interval saving
intervalId = setInterval(save, interval);
// Set up debounced input saving
form.addEventListener('input', function () {
if (debounceId) clearTimeout(debounceId);
debounceId = setTimeout(save, 1000);
});
// Clear on successful submit
form.addEventListener('submit', function () {
clear();
if (intervalId) clearInterval(intervalId);
});
// Restore on load
restore();
return { save, restore, clear };
}
// SOLUTION 10: Multi-step Form Wizard
class FormWizardSolution {
constructor(formElement, steps) {
this.form = formElement;
this.steps = steps;
this.currentStepIndex = 0;
this._setupSteps();
this._showStep(0);
}
_setupSteps() {
// Hide all steps except first
this.steps.forEach((step, index) => {
const stepElement = this.form.querySelector(`#${step.id}`);
if (stepElement) {
stepElement.style.display = index === 0 ? 'block' : 'none';
}
});
}
_showStep(index) {
this.steps.forEach((step, i) => {
const stepElement = this.form.querySelector(`#${step.id}`);
if (stepElement) {
stepElement.style.display = i === index ? 'block' : 'none';
}
});
}
_getStepData(stepIndex) {
const step = this.steps[stepIndex];
const stepElement = this.form.querySelector(`#${step.id}`);
if (!stepElement) return {};
const inputs = stepElement.querySelectorAll('input, select, textarea');
const data = {};
inputs.forEach((input) => {
if (input.name) {
if (input.type === 'checkbox') {
data[input.name] = input.checked;
} else if (input.type === 'radio') {
if (input.checked) data[input.name] = input.value;
} else {
data[input.name] = input.value;
}
}
});
return data;
}
getCurrentStep() {
return this.currentStepIndex;
}
nextStep() {
const currentStep = this.steps[this.currentStepIndex];
const data = this._getStepData(this.currentStepIndex);
// Validate current step
if (currentStep.validate && !currentStep.validate(data)) {
return { success: false, errors: ['Validation failed'] };
}
if (this.currentStepIndex < this.steps.length - 1) {
this.currentStepIndex++;
this._showStep(this.currentStepIndex);
return { success: true };
}
return { success: false, errors: ['Already at last step'] };
}
prevStep() {
if (this.currentStepIndex > 0) {
this.currentStepIndex--;
this._showStep(this.currentStepIndex);
return true;
}
return false;
}
goToStep(index) {
// Can only go to completed or current steps
if (
index <= this.currentStepIndex &&
index >= 0 &&
index < this.steps.length
) {
this.currentStepIndex = index;
this._showStep(index);
return true;
}
return false;
}
submit() {
// Validate all steps
for (let i = 0; i <= this.currentStepIndex; i++) {
const step = this.steps[i];
const data = this._getStepData(i);
if (step.validate && !step.validate(data)) {
return {
success: false,
errors: [`Step ${i + 1} validation failed`],
failedStep: i,
};
}
}
// Collect all data
const allData = {};
this.steps.forEach((_, index) => {
Object.assign(allData, this._getStepData(index));
});
return { success: true, data: allData };
}
}
// SOLUTION 11: Dependent Dropdowns
function setupDependentDropdownsSolution(config) {
const { parentSelect, childSelect, options } = config;
function updateChildOptions() {
const parentValue = parentSelect.value;
const childOptions = options[parentValue] || [];
// Clear existing options
childSelect.innerHTML = '<option value="">Select...</option>';
// Add new options
childOptions.forEach((opt) => {
const option = document.createElement('option');
option.value = opt.value;
option.textContent = opt.label;
childSelect.appendChild(option);
});
// Enable/disable based on options
childSelect.disabled = childOptions.length === 0;
}
parentSelect.addEventListener('change', updateChildOptions);
// Initialize
updateChildOptions();
return { update: updateChildOptions };
}
// SOLUTION 12: File Upload with Preview
function setupFileUploadSolution(inputElement, options = {}) {
const {
maxSize = Infinity,
allowedTypes = [],
maxFiles = Infinity,
previewContainer = null,
onValidationError = () => {},
onFilesSelected = () => {},
} = options;
let selectedFiles = [];
function validateFile(file) {
const errors = [];
if (file.size > maxSize) {
errors.push(
`${file.name} exceeds max size (${(maxSize / 1024 / 1024).toFixed(
1
)}MB)`
);
}
if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
errors.push(`${file.name} has invalid type (${file.type})`);
}
return errors;
}
function createPreview(file, index) {
const preview = document.createElement('div');
preview.className = 'file-preview';
preview.style.cssText =
'display: inline-block; margin: 10px; position: relative;';
if (file.type.startsWith('image/')) {
const img = document.createElement('img');
img.style.cssText = 'max-width: 150px; max-height: 150px;';
const reader = new FileReader();
reader.onload = (e) => {
img.src = e.target.result;
};
reader.readAsDataURL(file);
preview.appendChild(img);
} else {
const info = document.createElement('div');
info.style.cssText =
'padding: 20px; background: #f0f0f0; border-radius: 4px;';
info.textContent = `📄 ${file.name}`;
preview.appendChild(info);
}
// Remove button
const removeBtn = document.createElement('button');
removeBtn.textContent = '×';
removeBtn.style.cssText =
'position: absolute; top: -5px; right: -5px; ' +
'background: red; color: white; border: none; border-radius: 50%; ' +
'width: 20px; height: 20px; cursor: pointer;';
removeBtn.onclick = () => {
selectedFiles.splice(index, 1);
updatePreviews();
};
preview.appendChild(removeBtn);
return preview;
}
function updatePreviews() {
if (!previewContainer) return;
previewContainer.innerHTML = '';
selectedFiles.forEach((file, index) => {
previewContainer.appendChild(createPreview(file, index));
});
}
inputElement.addEventListener('change', function () {
const files = Array.from(this.files);
const errors = [];
// Check max files
if (files.length > maxFiles) {
errors.push(`Maximum ${maxFiles} files allowed`);
}
// Validate each file
const validFiles = [];
files.forEach((file) => {
const fileErrors = validateFile(file);
if (fileErrors.length > 0) {
errors.push(...fileErrors);
} else {
validFiles.push(file);
}
});
if (errors.length > 0) {
onValidationError(errors);
}
selectedFiles = validFiles.slice(0, maxFiles);
updatePreviews();
onFilesSelected(selectedFiles);
});
return {
getFiles: () => selectedFiles,
clear: () => {
selectedFiles = [];
inputElement.value = '';
updatePreviews();
},
};
}
// SOLUTION 13: Form Field Array
function createFieldArraySolution(
containerElement,
fieldTemplate,
options = {}
) {
const {
minItems = 0,
maxItems = Infinity,
onAdd = () => {},
onRemove = () => {},
sortable = false,
} = options;
let itemCount = 0;
function createItem() {
const wrapper = document.createElement('div');
wrapper.className = 'field-array-item';
wrapper.dataset.index = itemCount++;
wrapper.innerHTML = fieldTemplate;
// Add remove button
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.textContent = 'Remove';
removeBtn.className = 'remove-item';
wrapper.appendChild(removeBtn);
return wrapper;
}
function add() {
const items = containerElement.querySelectorAll('.field-array-item');
if (items.length >= maxItems) {
console.log(`Maximum ${maxItems} items allowed`);
return false;
}
const newItem = createItem();
containerElement.appendChild(newItem);
onAdd(newItem);
return true;
}
function remove(index) {
const items = containerElement.querySelectorAll('.field-array-item');
if (items.length <= minItems) {
console.log(`Minimum ${minItems} items required`);
return false;
}
const item = items[index];
if (item) {
onRemove(item);
item.remove();
return true;
}
return false;
}
function getValues() {
const items = containerElement.querySelectorAll('.field-array-item');
return Array.from(items).map((item) => {
const input = item.querySelector('input, select, textarea');
return input ? input.value : null;
});
}
function setValues(values) {
containerElement.innerHTML = '';
itemCount = 0;
values.forEach((value) => {
const item = createItem();
const input = item.querySelector('input, select, textarea');
if (input) input.value = value;
containerElement.appendChild(item);
});
}
// Event delegation for remove buttons
containerElement.addEventListener('click', function (e) {
if (e.target.classList.contains('remove-item')) {
const item = e.target.closest('.field-array-item');
const items = Array.from(
containerElement.querySelectorAll('.field-array-item')
);
const index = items.indexOf(item);
remove(index);
}
});
// Initialize with minimum items
for (let i = 0; i < minItems; i++) {
add();
}
return { add, remove, getValues, setValues };
}
// SOLUTION 14: Credit Card Form (partial)
class CreditCardFormSolution {
constructor(formElement) {
this.form = formElement;
this.cardNumberInput = formElement.querySelector('[name="cardNumber"]');
this.expiryInput = formElement.querySelector('[name="expiry"]');
this.cvvInput = formElement.querySelector('[name="cvv"]');
this._setupEventListeners();
}
_setupEventListeners() {
if (this.cardNumberInput) {
this.cardNumberInput.addEventListener('input', (e) => {
e.target.value = this.formatCardNumber(e.target.value);
});
}
if (this.expiryInput) {
this.expiryInput.addEventListener('input', (e) => {
let value = e.target.value.replace(/\D/g, '');
if (value.length >= 2) {
value = value.slice(0, 2) + '/' + value.slice(2, 4);
}
e.target.value = value;
});
}
if (this.cvvInput) {
this.cvvInput.addEventListener('input', (e) => {
e.target.value = e.target.value.replace(/\D/g, '').slice(0, 4);
});
}
}
detectCardType(number) {
const clean = number.replace(/\s/g, '');
if (/^4/.test(clean)) return 'visa';
if (/^5[1-5]/.test(clean)) return 'mastercard';
if (/^3[47]/.test(clean)) return 'amex';
if (/^6011/.test(clean)) return 'discover';
return 'unknown';
}
validateCardNumber(number) {
const clean = number.replace(/\s/g, '');
if (!/^\d+$/.test(clean)) return false;
// Luhn algorithm
let sum = 0;
let isEven = false;
for (let i = clean.length - 1; i >= 0; i--) {
let digit = parseInt(clean[i], 10);
if (isEven) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
isEven = !isEven;
}
return sum % 10 === 0;
}
validateExpiry(expiry) {
const match = expiry.match(/^(\d{2})\/(\d{2})$/);
if (!match) return false;
const month = parseInt(match[1], 10);
const year = parseInt('20' + match[2], 10);
if (month < 1 || month > 12) return false;
const now = new Date();
const currentMonth = now.getMonth() + 1;
const currentYear = now.getFullYear();
if (year < currentYear) return false;
if (year === currentYear && month < currentMonth) return false;
return true;
}
formatCardNumber(number) {
const clean = number.replace(/\D/g, '');
const groups = clean.match(/.{1,4}/g) || [];
return groups.join(' ').slice(0, 19);
}
getCardData() {
const number = this.cardNumberInput?.value || '';
const expiry = this.expiryInput?.value || '';
const cvv = this.cvvInput?.value || '';
return {
number: number.replace(/\s/g, ''),
expiry,
cvv,
type: this.detectCardType(number),
valid:
this.validateCardNumber(number) &&
this.validateExpiry(expiry) &&
cvv.length >= 3,
};
}
}
// SOLUTION 15: Form Submission Handler
function createFormSubmitHandlerSolution(form, options) {
const {
url,
method = 'POST',
headers = {},
transformData = (data) => data,
validate = () => ({ valid: true }),
onSubmitStart = () => {},
onSuccess = () => {},
onError = () => {},
onComplete = () => {},
retries = 0,
retryDelay = 1000,
} = options;
let abortController = null;
async function submit() {
const formData = new FormData(form);
// Validate
const validation = validate(Object.fromEntries(formData));
if (!validation.valid) {
onError(new Error(validation.error || 'Validation failed'));
return;
}
onSubmitStart();
abortController = new AbortController();
let lastError;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
const response = await fetch(url, {
method,
headers,
body: transformData(formData),
signal: abortController.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
onSuccess(result);
onComplete();
return result;
} catch (error) {
lastError = error;
if (error.name === 'AbortError') {
onError(new Error('Request aborted'));
onComplete();
return;
}
if (attempt < retries) {
console.log(`Retry ${attempt + 1}/${retries} in ${retryDelay}ms...`);
await new Promise((resolve) => setTimeout(resolve, retryDelay));
}
}
}
onError(lastError);
onComplete();
}
function abort() {
if (abortController) {
abortController.abort();
abortController = null;
}
}
// Attach to form submit
form.addEventListener('submit', (e) => {
e.preventDefault();
submit();
});
return { submit, abort };
}
console.log('Forms and User Input exercises loaded!');
console.log(
'Complete each exercise and check against solutions at the bottom.'
);