JavaScript and TypeScript course Β· Module 8: Events and the DOM
Form Events
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. What does 'this' refer to in a traditional event handler function?
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: