JavaScript and TypeScript course Β· Module 8: Events and the DOM

Form Events

5 min read

Forms in Jurassic Park are used for guest registration, staff management, and security systems. Similarly, in web applications, forms are key for interacting with users.

Imagine a booking form for a tour of the T-Rex enclosure. Without JavaScript, clicking "Send" makes the browser reload the whole page, and the guest only finds out about a typo in their email address when the confirmation never arrives. Form events let you take over this process and react before anything leaves the browser.

Handling Forms

The most important signal is submit. A form sends it when the guest clicks the submit button or presses Enter in a text field. We attach the listener to the form itself, not to the button, so that we catch both.

1const ticketBookingForm = document.getElementById('booking-form');
2const emailInput = document.getElementById('email');
3const phoneInput = document.getElementById('phone');
4const ticketTypeSelect = document.getElementById('ticket-type');
5
6// Submit - form submission
7ticketBookingForm.addEventListener('submit', (e) => {
8  e.preventDefault();
9
10  const formData = new FormData(ticketBookingForm);
11  const bookingData = {
12    name: formData.get('name'),
13    email: formData.get('email'),
14    phone: formData.get('phone'),
15    ticketType: formData.get('ticket-type'),
16    visitDate: formData.get('visit-date'),
17    guestCount: parseInt(formData.get('guest-count'))
18  };
19
20  if (validateBookingData(bookingData)) {
21    submitBooking(bookingData);
22  }
23});

Handling a submission always has four phases in the same order. First we intercept the submit event, then we call preventDefault() to stop the default submission and page reload, next we validate the data, and finally we send it ourselves. The FormData object collects field values by their name attributes, and text and number fields hand them over as text, which is why the guest count goes through parseInt().

You can do the sending with the fetch() function you know from the lesson on asynchronous programming. Here is a sample version of submitBooking() that sends the booking as JSON.

1async function submitBooking(bookingData) {
2  const response = await fetch('/api/bookings', {
3    method: 'POST',
4    headers: { 'Content-Type': 'application/json' },
5    body: JSON.stringify(bookingData)
6  });
7
8  if (!response.ok) {
9    console.error('Booking rejected:', response.status);
10    return;
11  }
12  console.log('Booking accepted!');
13}

The page never reloads, and the guest stays on the form. Remember that fetch() throws an error only when there is a network problem. A response with a 400 or 500 code is a successful delivery as far as it is concerned, so we check response.ok ourselves.

Real-Time Validation

The input event fires on every change of the field's value: every typed character, paste, or deletion. Before we use it, we need a function that checks the email address.

1function isValidEmail(email) {
2  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
3}

It's a simplified guard: it requires an @ sign and a dot in the domain, but it doesn't check whether the mailbox exists. Now we connect it to the field so that the CSS class changes as the guest types.

1// Input - field value change
2emailInput.addEventListener('input', (e) => {
3  const email = e.target.value;
4  if (isValidEmail(email)) {
5    e.target.classList.remove('invalid');
6    e.target.classList.add('valid');
7  } else {
8    e.target.classList.remove('valid');
9    e.target.classList.add('invalid');
10  }
11});

The guest sees a red or green border while typing, and the field's value stays untouched, because we only change its class.

The change Event

The change event fires less often, only once the change is committed. For a <select> list this happens right after choosing an option, and for a text field only after leaving it.

1// Change - committed change (select: right after choosing, text field: after leaving it)
2ticketTypeSelect.addEventListener('change', (e) => {
3  const selectedType = e.target.value;
4  updateTicketPrice(selectedType);
5  showTicketTypeInfo(selectedType);
6});

change is a perfect fit for recalculating the ticket price, because we react to the guest's decision rather than to every moment of hesitation. My rule: input for live hints, change for reacting to a choice, and submit for the final check. Leave the simplest rules, like required or type="email", to HTML attributes. The browser will block an invalid form on its own and won't even send submit in that case.

The this Context in Event Handlers

An important aspect of event handlers is the this context. In a traditional function (function expression), this refers to the element on which the event listener was registered - that is, the element to which the handler is "attached."

1const submitBtn = document.getElementById('submit-btn');
2
3// Traditional function - this points to the element with the listener
4submitBtn.addEventListener('click', function(event) {
5  console.log(this);          // <button id="submit-btn">
6  console.log(this === event.currentTarget); // true
7  console.log(event.target);  // the element that was actually clicked
8
9  // this is the element with the listener, event.target is the clicked element
10  this.textContent = 'Submitted!';
11  this.disabled = true;
12});

Here this equals event.currentTarget, which is why the button can change its own text and disable itself. event.target points to the element that was actually clicked, and for a button with plain text it's the same element. An arrow function behaves differently.

1// Arrow function - this does NOT point to the element!
2submitBtn.addEventListener('click', (event) => {
3  console.log(this); // this from the surrounding code, e.g. window (in an ES module: undefined)
4  // Arrow functions don't have their own this
5  // Use event.currentTarget instead of this
6  event.currentTarget.textContent = 'Submitted!';
7});

An arrow function has no this of its own, so it takes it from the surrounding code. At the top level of a regular script that will be window, and in an ES module the value undefined. You definitely won't get the button here, which is why arrow functions reach for event.currentTarget.

"Remember," Ray warns, "arrow functions don't have their own this. If you need to refer to the element via this, use traditional functions. If you prefer arrow functions, use event.currentTarget."

I go with arrow functions and event.currentTarget, because such code works the same no matter where you move it. In the sandbox below, a tour booking form awaits you with all three events, plus focus and blur, which fire when a field gains and loses focus. In the next lesson you'll learn to clean up after listeners when a form disappears from the page.

Remember: a form is the gate to the park, and submit with preventDefault() is the guard who checks the pass before anyone goes any further.

Code for this lesson: index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Form events - Tour booking</title>
7    <style>
8        * {
9            margin: 0;
10            padding: 0;
11            box-sizing: border-box;
12        }
13
14        body {
15            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16            background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
17            padding: 20px;
18            color: white;
19            min-height: 100vh;
20            display: flex;
21            align-items: center;
22            justify-content: center;
23        }
24
25        .form-container {
26            max-width: 600px;
27            width: 100%;
28            background: rgba(0, 0, 0, 0.8);
29            border-radius: 15px;
30            padding: 40px;
31            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
32        }
33
34        h1 {
35            text-align: center;
36            margin-bottom: 10px;
37            color: #ecf0f1;
38        }
39
40        .subtitle {
41            text-align: center;
42            color: #bdc3c7;
43            margin-bottom: 30px;
44        }
45
46        .form-group {
47            margin-bottom: 25px;
48        }
49
50        label {
51            display: block;
52            margin-bottom: 8px;
53            color: #ecf0f1;
54            font-weight: 500;
55        }
56
57        .required {
58            color: #e74c3c;
59        }
60
61        input, select, textarea {
62            width: 100%;
63            padding: 12px 15px;
64            border: 2px solid #34495e;
65            border-radius: 8px;
66            background: rgba(255, 255, 255, 0.1);
67            color: white;
68            font-size: 16px;
69            transition: all 0.3s ease;
70        }
71
72        input:focus, select:focus, textarea:focus {
73            outline: none;
74            border-color: #3498db;
75            background: rgba(255, 255, 255, 0.15);
76        }
77
78        input.valid {
79            border-color: #27ae60;
80            background: rgba(39, 174, 96, 0.1);
81        }
82
83        input.invalid {
84            border-color: #e74c3c;
85            background: rgba(231, 76, 60, 0.1);
86        }
87
88        .error-message {
89            color: #e74c3c;
90            font-size: 14px;
91            margin-top: 5px;
92            display: none;
93        }
94
95        .input-hint {
96            color: #95a5a6;
97            font-size: 13px;
98            margin-top: 5px;
99        }
100
101        .char-counter {
102            text-align: right;
103            color: #95a5a6;
104            font-size: 12px;
105            margin-top: 5px;
106        }
107
108        .submit-btn {
109            width: 100%;
110            padding: 15px;
111            background: linear-gradient(45deg, #27ae60, #2ecc71);
112            color: white;
113            border: none;
114            border-radius: 8px;
115            font-size: 18px;
116            font-weight: bold;
117            cursor: pointer;
118            transition: all 0.3s ease;
119            margin-top: 20px;
120        }
121
122        .submit-btn:hover:not(:disabled) {
123            transform: translateY(-2px);
124            box-shadow: 0 10px 25px rgba(39, 174, 96, 0.3);
125        }
126
127        .submit-btn:disabled {
128            background: #7f8c8d;
129            cursor: not-allowed;
130        }
131
132        .success-message {
133            background: linear-gradient(45deg, #27ae60, #2ecc71);
134            color: white;
135            padding: 20px;
136            border-radius: 10px;
137            text-align: center;
138            display: none;
139            animation: slideIn 0.5s ease;
140        }
141
142        @keyframes slideIn {
143            from {
144                opacity: 0;
145                transform: translateY(-20px);
146            }
147            to {
148                opacity: 1;
149                transform: translateY(0);
150            }
151        }
152
153        .price-info {
154            background: rgba(52, 152, 219, 0.2);
155            border: 1px solid #3498db;
156            border-radius: 8px;
157            padding: 15px;
158            margin: 20px 0;
159            text-align: center;
160        }
161
162        .price-value {
163            font-size: 32px;
164            font-weight: bold;
165            color: #3498db;
166        }
167
168        .event-log {
169            background: rgba(0, 0, 0, 0.6);
170            border-radius: 8px;
171            padding: 15px;
172            margin-top: 20px;
173            max-height: 200px;
174            overflow-y: auto;
175            font-family: 'Courier New', monospace;
176            font-size: 12px;
177        }
178
179        .log-entry {
180            padding: 3px 0;
181            border-bottom: 1px solid rgba(255, 255, 255, 0.1);
182            color: #95a5a6;
183        }
184    </style>
185</head>
186<body>
187    <div class="form-container">
188        <h1>Tour Booking</h1>
189        <p class="subtitle">Jurassic Park - Discover the world of dinosaurs</p>
190
191        <form id="booking-form">
192            <div class="form-group">
193                <label for="name">Full name <span class="required">*</span></label>
194                <input type="text" id="name" name="name" required>
195                <div class="error-message" id="name-error">Enter full name (min. 3 characters)</div>
196            </div>
197
198            <div class="form-group">
199                <label for="email">Email <span class="required">*</span></label>
200                <input type="email" id="email" name="email" required>
201                <div class="error-message" id="email-error">Enter a valid email address</div>
202                <div class="input-hint">We will send reservation confirmation to this address</div>
203            </div>
204
205            <div class="form-group">
206                <label for="phone">Contact phone <span class="required">*</span></label>
207                <input type="tel" id="phone" name="phone" placeholder="+48 123 456 789" required>
208                <div class="error-message" id="phone-error">Enter a valid phone number</div>
209            </div>
210
211            <div class="form-group">
212                <label for="date">Tour date <span class="required">*</span></label>
213                <input type="date" id="date" name="date" required>
214                <div class="error-message" id="date-error">Choose a future date</div>
215            </div>
216
217            <div class="form-group">
218                <label for="tour-type">Tour type <span class="required">*</span></label>
219                <select id="tour-type" name="tour-type" required>
220                    <option value="">Choose a tour type</option>
221                    <option value="standard" data-price="150">Standard (2h) - 150 USD</option>
222                    <option value="vip" data-price="300">VIP with guide (4h) - 300 USD</option>
223                    <option value="night" data-price="250">Night tour (3h) - 250 USD</option>
224                    <option value="family" data-price="500">Family (4h, max 4 people) - 500 USD</option>
225                </select>
226            </div>
227
228            <div class="form-group">
229                <label for="guests">Number of guests <span class="required">*</span></label>
230                <input type="number" id="guests" name="guests" min="1" max="20" value="1" required>
231                <div class="input-hint">Min. 1, max. 20 guests</div>
232            </div>
233
234            <div class="form-group">
235                <label for="comments">Additional comments</label>
236                <textarea id="comments" name="comments" rows="4" maxlength="500"></textarea>
237                <div class="char-counter" id="char-counter">0 / 500 characters</div>
238            </div>
239
240            <div class="price-info">
241                <div style="color: #bdc3c7; margin-bottom: 10px;">Total cost:</div>
242                <div class="price-value" id="total-price">0 USD</div>
243            </div>
244
245            <button type="submit" class="submit-btn" id="submit-btn" disabled>
246                Book a tour
247            </button>
248        </form>
249
250        <div class="success-message" id="success-message">
251            <h2>Booking confirmed!</h2>
252            <p>Thank you for your reservation. Details have been sent to the provided email address.</p>
253        </div>
254
255        <div class="event-log" id="event-log">
256            <strong>Form event log:</strong>
257        </div>
258    </div>
259
260    <script>
261        console.log('Jurassic Park booking system - ONLINE');
262
263        // References to elements
264        const form = document.getElementById('booking-form');
265        const nameInput = document.getElementById('name');
266        const emailInput = document.getElementById('email');
267        const phoneInput = document.getElementById('phone');
268        const dateInput = document.getElementById('date');
269        const tourTypeSelect = document.getElementById('tour-type');
270        const guestsInput = document.getElementById('guests');
271        const commentsTextarea = document.getElementById('comments');
272        const charCounter = document.getElementById('char-counter');
273        const totalPriceDisplay = document.getElementById('total-price');
274        const submitBtn = document.getElementById('submit-btn');
275        const successMessage = document.getElementById('success-message');
276        const eventLog = document.getElementById('event-log');
277
278        // Logging function
279        function logEvent(message) {
280            const entry = document.createElement('div');
281            entry.className = 'log-entry';
282            entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
283            eventLog.appendChild(entry);
284            eventLog.scrollTop = eventLog.scrollHeight;
285            console.log(message);
286        }
287
288        // Validation functions
289        function validateName(name) {
290            return name.trim().length >= 3;
291        }
292
293        function validateEmail(email) {
294            const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
295            return regex.test(email);
296        }
297
298        function validatePhone(phone) {
299            const cleaned = phone.replace(/\s/g, '');
300            return cleaned.length >= 9;
301        }
302
303        function validateDate(date) {
304            const selected = new Date(date);
305            const today = new Date();
306            today.setHours(0, 0, 0, 0);
307            return selected > today;
308        }
309
310        function showError(input, errorId, show) {
311            const errorElement = document.getElementById(errorId);
312            if (show) {
313                errorElement.style.display = 'block';
314                input.classList.add('invalid');
315                input.classList.remove('valid');
316            } else {
317                errorElement.style.display = 'none';
318                input.classList.remove('invalid');
319                input.classList.add('valid');
320            }
321        }
322
323        function updateTotalPrice() {
324            const selectedOption = tourTypeSelect.options[tourTypeSelect.selectedIndex];
325            const pricePerPerson = parseInt(selectedOption.dataset.price) || 0;
326            const guests = parseInt(guestsInput.value) || 0;
327            const total = pricePerPerson * guests;
328            totalPriceDisplay.textContent = `${total} USD`;
329            logEvent(`Updated price: ${total} USD`);
330        }
331
332        function checkFormValidity() {
333            const isValid = validateName(nameInput.value) &&
334                          validateEmail(emailInput.value) &&
335                          validatePhone(phoneInput.value) &&
336                          validateDate(dateInput.value) &&
337                          tourTypeSelect.value !== '' &&
338                          guestsInput.value >= 1;
339            submitBtn.disabled = !isValid;
340        }
341
342        // INPUT EVENT - real-time validation
343        nameInput.addEventListener('input', function(e) {
344            const isValid = validateName(e.target.value);
345            showError(nameInput, 'name-error', !isValid);
346            logEvent(`Typing name: ${e.target.value.length} characters`);
347            checkFormValidity();
348        });
349
350        emailInput.addEventListener('input', function(e) {
351            const isValid = validateEmail(e.target.value);
352            showError(emailInput, 'email-error', !isValid);
353            logEvent(`Typing email: ${e.target.value}`);
354            checkFormValidity();
355        });
356
357        phoneInput.addEventListener('input', function(e) {
358            const isValid = validatePhone(e.target.value);
359            showError(phoneInput, 'phone-error', !isValid);
360            checkFormValidity();
361        });
362
363        // CHANGE EVENT - value change
364        dateInput.addEventListener('change', function(e) {
365            const isValid = validateDate(e.target.value);
366            showError(dateInput, 'date-error', !isValid);
367            logEvent(`Date selected: ${e.target.value}`);
368            checkFormValidity();
369        });
370
371        tourTypeSelect.addEventListener('change', function(e) {
372            const selectedText = e.target.options[e.target.selectedIndex].text;
373            logEvent(`Tour type selected: ${selectedText}`);
374            updateTotalPrice();
375            checkFormValidity();
376        });
377
378        guestsInput.addEventListener('change', function(e) {
379            logEvent(`Changed number of guests: ${e.target.value}`);
380            updateTotalPrice();
381        });
382
383        // FOCUS/BLUR EVENTS
384        const allInputs = [nameInput, emailInput, phoneInput, dateInput, tourTypeSelect, guestsInput, commentsTextarea];
385
386        allInputs.forEach(input => {
387            input.addEventListener('focus', function() {
388                logEvent(`Focus on: ${this.name || this.id}`);
389            });
390
391            input.addEventListener('blur', function() {
392                logEvent(`Blur from: ${this.name || this.id}`);
393            });
394        });
395
396        // Character counter for the textarea
397        commentsTextarea.addEventListener('input', function(e) {
398            const length = e.target.value.length;
399            const maxLength = e.target.maxLength;
400            charCounter.textContent = `${length} / ${maxLength} characters`;
401
402            if (length > maxLength - 50) {
403                charCounter.style.color = '#e74c3c';
404            } else {
405                charCounter.style.color = '#95a5a6';
406            }
407        });
408
409        // SUBMIT EVENT
410        form.addEventListener('submit', function(e) {
411            e.preventDefault();
412
413            if (submitBtn.disabled) {
414                alert('Form contains errors. Fix them before submitting.');
415                return;
416            }
417
418            const formData = new FormData(form);
419            const data = {
420                name: formData.get('name'),
421                email: formData.get('email'),
422                phone: formData.get('phone'),
423                date: formData.get('date'),
424                tourType: formData.get('tour-type'),
425                guests: formData.get('guests'),
426                comments: formData.get('comments') || 'None'
427            };
428
429            logEvent(`Submitting form...\nData: ${JSON.stringify(data, null, 2)}`);
430
431            submitBtn.textContent = 'Booking...';
432            submitBtn.disabled = true;
433
434            setTimeout(() => {
435                form.style.display = 'none';
436                successMessage.style.display = 'block';
437                logEvent('Reservation successfully registered!');
438
439                console.log('Booking data:', data);
440
441                setTimeout(() => {
442                    form.style.display = 'block';
443                    successMessage.style.display = 'none';
444                    form.reset();
445                    submitBtn.textContent = 'Book a tour';
446                    totalPriceDisplay.textContent = '0 USD';
447                    allInputs.forEach(input => input.classList.remove('valid', 'invalid'));
448                    checkFormValidity();
449                }, 3000);
450            }, 2000);
451        });
452
453        // Set minimum date to tomorrow
454        const tomorrow = new Date();
455        tomorrow.setDate(tomorrow.getDate() + 1);
456        dateInput.min = tomorrow.toISOString().split('T')[0];
457
458        logEvent('Reservation system loaded');
459    </script>
460</body>
461</html>

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What does 'this' refer to in a traditional event handler function?

  2. 2. What should be done to prevent the default page reload when submitting a form?

Hands-on tasks in the game

  • Code editor

    The index.html file has a #navbar bar, and style.css hides it when it has the hidden class. The script remembers the last scroll position (window.scrollY). Fill in the blanks: ___BLANK1___ is the page scroll event listened to on window, and ___BLANK2___ is the comparison operator that detects scrolling down (the new position is greater than the previous one). The bar should hide when scrolling down and come back when scrolling up.

  • Click in order

    Arrange the code for real-time input validation.

  • Vertical ordering

    Arrange the order of form submit event handling:

Useful articles