javascript
exercises
exercises.js⚡javascript
/**
* ============================================================
* 10.4 Event Delegation - Exercises
* ============================================================
*
* Practice event delegation patterns for efficient
* event handling and dynamic content.
*
* Instructions:
* 1. Read each exercise description carefully
* 2. Write your solution in the provided function
* 3. Test with the provided HTML structure
* 4. Check the hints and solutions if needed
*/
// =============================================================
// EXERCISE 1: Basic List Delegation
// =============================================================
/**
* Add a delegated click handler to a list
* When any list item is clicked, log its text content
*
* @param {string} listId - ID of the <ul> or <ol> element
* @returns {boolean} - True if successful
*
* Example HTML: <ul id="myList"><li>Item 1</li><li>Item 2</li></ul>
*/
function delegateListClicks(listId) {
// Your code here
}
// Test
// delegateListClicks("myList");
/*
* HINT: Check if event.target.tagName === "LI"
*
* SOLUTION:
* function delegateListClicks(listId) {
* const list = document.getElementById(listId);
* if (!list) return false;
*
* list.addEventListener("click", (event) => {
* if (event.target.tagName === "LI") {
* console.log("Clicked:", event.target.textContent);
* }
* });
*
* return true;
* }
*/
// =============================================================
// EXERCISE 2: Button Group Handler
// =============================================================
/**
* Handle clicks on buttons within a container
* Buttons have data-value attribute
* Return the selected value
*
* @param {string} containerId - ID of the button container
* @param {Function} onSelect - Callback with selected value
* @returns {boolean} - True if successful
*
* Example HTML:
* <div id="options">
* <button data-value="a">Option A</button>
* <button data-value="b">Option B</button>
* </div>
*/
function handleButtonGroup(containerId, onSelect) {
// Your code here
}
// Test
// handleButtonGroup("options", (value) => console.log("Selected:", value));
/*
* HINT: Use closest("[data-value]") to find the button
*
* SOLUTION:
* function handleButtonGroup(containerId, onSelect) {
* const container = document.getElementById(containerId);
* if (!container) return false;
*
* container.addEventListener("click", (event) => {
* const button = event.target.closest("[data-value]");
*
* if (button) {
* // Remove active from siblings
* container.querySelectorAll("[data-value]").forEach(btn => {
* btn.classList.remove("active");
* });
*
* // Add active to selected
* button.classList.add("active");
*
* onSelect(button.dataset.value);
* }
* });
*
* return true;
* }
*/
// =============================================================
// EXERCISE 3: Nested Content Handling
// =============================================================
/**
* Handle clicks on cards with nested content
* Cards have structure: .card > .card-header + .card-body + .card-actions
* Only handle clicks on .card-actions buttons
*
* @param {string} containerId - ID of cards container
* @param {Object} handlers - { edit: fn, delete: fn, view: fn }
* @returns {boolean} - True if successful
*/
function handleCardActions(containerId, handlers) {
// Your code here
}
// Test
// handleCardActions("cards", {
// edit: (id) => console.log("Edit:", id),
// delete: (id) => console.log("Delete:", id),
// view: (id) => console.log("View:", id)
// });
/*
* HINT: Use closest(".card") to get the card, check button classes
*
* SOLUTION:
* function handleCardActions(containerId, handlers) {
* const container = document.getElementById(containerId);
* if (!container) return false;
*
* container.addEventListener("click", (event) => {
* const button = event.target.closest("button");
* const card = event.target.closest(".card");
*
* if (!button || !card) return;
*
* const cardId = card.dataset.id;
*
* if (button.classList.contains("edit-btn") && handlers.edit) {
* handlers.edit(cardId);
* } else if (button.classList.contains("delete-btn") && handlers.delete) {
* handlers.delete(cardId);
* } else if (button.classList.contains("view-btn") && handlers.view) {
* handlers.view(cardId);
* }
* });
*
* return true;
* }
*/
// =============================================================
// EXERCISE 4: Dynamic Todo List
// =============================================================
/**
* Create a todo list with delegation for:
* - Toggle complete on checkbox click
* - Delete on delete button click
* - Edit on double-click on text
*
* @param {string} listId - ID of the todo list
* @returns {Object} - { addTodo(text), getTodos(), clearCompleted() }
*/
function createTodoList(listId) {
// Your code here
}
// Test
// const todos = createTodoList("todo-list");
// todos.addTodo("Learn delegation");
// todos.addTodo("Practice JavaScript");
/*
* HINT: Set up multiple delegated handlers for different interactions
*
* SOLUTION:
* function createTodoList(listId) {
* const list = document.getElementById(listId);
* if (!list) return null;
*
* let todoId = 0;
*
* // Delegated click handler
* list.addEventListener("click", (event) => {
* const item = event.target.closest(".todo-item");
* if (!item) return;
*
* if (event.target.matches(".todo-checkbox")) {
* item.classList.toggle("completed");
* } else if (event.target.matches(".todo-delete")) {
* item.remove();
* }
* });
*
* // Double-click to edit
* list.addEventListener("dblclick", (event) => {
* const text = event.target.closest(".todo-text");
* if (!text) return;
*
* const currentText = text.textContent;
* const input = document.createElement("input");
* input.value = currentText;
* input.className = "todo-edit";
*
* text.replaceWith(input);
* input.focus();
*
* input.addEventListener("blur", () => {
* const newText = document.createElement("span");
* newText.className = "todo-text";
* newText.textContent = input.value || currentText;
* input.replaceWith(newText);
* });
*
* input.addEventListener("keydown", (e) => {
* if (e.key === "Enter") input.blur();
* if (e.key === "Escape") {
* input.value = currentText;
* input.blur();
* }
* });
* });
*
* return {
* addTodo(text) {
* const item = document.createElement("li");
* item.className = "todo-item";
* item.dataset.id = ++todoId;
* item.innerHTML = `
* <input type="checkbox" class="todo-checkbox">
* <span class="todo-text">${text}</span>
* <button class="todo-delete">×</button>
* `;
* list.appendChild(item);
* return todoId;
* },
*
* getTodos() {
* return Array.from(list.querySelectorAll(".todo-item")).map(item => ({
* id: item.dataset.id,
* text: item.querySelector(".todo-text")?.textContent,
* completed: item.classList.contains("completed")
* }));
* },
*
* clearCompleted() {
* list.querySelectorAll(".todo-item.completed").forEach(item => item.remove());
* }
* };
* }
*/
// =============================================================
// EXERCISE 5: Action Router
// =============================================================
/**
* Create an action router that handles data-action attributes
*
* @param {string} containerId - ID of the container
* @param {Object} actions - Object mapping action names to handlers
* @returns {Object} - { addAction(name, handler), removeAction(name) }
*
* HTML usage: <button data-action="save">Save</button>
*/
function createActionRouter(containerId, actions = {}) {
// Your code here
}
// Test
// const router = createActionRouter("app", {
// save: () => console.log("Saving..."),
// cancel: () => console.log("Cancelled")
// });
// router.addAction("delete", () => console.log("Deleting..."));
/*
* HINT: Store actions in an object, look up by data-action value
*
* SOLUTION:
* function createActionRouter(containerId, actions = {}) {
* const container = document.getElementById(containerId);
* if (!container) return null;
*
* const actionMap = { ...actions };
*
* container.addEventListener("click", (event) => {
* const element = event.target.closest("[data-action]");
*
* if (element) {
* const action = element.dataset.action;
*
* if (actionMap[action]) {
* event.preventDefault();
* actionMap[action](element, event);
* }
* }
* });
*
* return {
* addAction(name, handler) {
* actionMap[name] = handler;
* },
*
* removeAction(name) {
* delete actionMap[name];
* }
* };
* }
*/
// =============================================================
// EXERCISE 6: Table with Sorting
// =============================================================
/**
* Make table headers clickable for sorting
* Use delegation on thead for header clicks
*
* @param {string} tableId - ID of the table
* @param {Function} onSort - Callback with (column, direction)
* @returns {Object} - { getSortState() }
*/
function createSortableTable(tableId, onSort) {
// Your code here
}
// Test
// const table = createSortableTable("data-table", (col, dir) => {
// console.log(`Sort by ${col} ${dir}`);
// });
/*
* HINT: Track current sort column and toggle direction on re-click
*
* SOLUTION:
* function createSortableTable(tableId, onSort) {
* const table = document.getElementById(tableId);
* if (!table) return null;
*
* const thead = table.querySelector("thead");
* if (!thead) return null;
*
* let currentColumn = null;
* let currentDirection = "asc";
*
* thead.addEventListener("click", (event) => {
* const th = event.target.closest("th[data-sort]");
*
* if (th) {
* const column = th.dataset.sort;
*
* // Toggle direction if same column
* if (column === currentColumn) {
* currentDirection = currentDirection === "asc" ? "desc" : "asc";
* } else {
* currentColumn = column;
* currentDirection = "asc";
* }
*
* // Update visual indicators
* thead.querySelectorAll("th").forEach(header => {
* header.classList.remove("sort-asc", "sort-desc");
* });
* th.classList.add(`sort-${currentDirection}`);
*
* onSort(column, currentDirection);
* }
* });
*
* return {
* getSortState() {
* return { column: currentColumn, direction: currentDirection };
* }
* };
* }
*/
// =============================================================
// EXERCISE 7: Accordion Component
// =============================================================
/**
* Create an accordion with delegated expand/collapse
*
* @param {string} containerId - ID of the accordion container
* @param {Object} options - { exclusive: boolean, animated: boolean }
* @returns {Object} - { expandAll(), collapseAll(), toggle(index) }
*/
function createAccordion(containerId, options = {}) {
// Your code here
}
// Test
// const accordion = createAccordion("faq", { exclusive: true, animated: true });
/*
* HINT: Toggle open class on accordion items, handle exclusive mode
*
* SOLUTION:
* function createAccordion(containerId, options = {}) {
* const container = document.getElementById(containerId);
* if (!container) return null;
*
* const { exclusive = false, animated = true } = options;
*
* function toggleItem(item, forceOpen = null) {
* const content = item.querySelector(".accordion-content");
* const isOpen = item.classList.contains("open");
* const shouldOpen = forceOpen !== null ? forceOpen : !isOpen;
*
* if (shouldOpen) {
* item.classList.add("open");
* if (animated) {
* content.style.maxHeight = content.scrollHeight + "px";
* }
* } else {
* item.classList.remove("open");
* if (animated) {
* content.style.maxHeight = "0";
* }
* }
* }
*
* container.addEventListener("click", (event) => {
* const header = event.target.closest(".accordion-header");
* if (!header) return;
*
* const item = header.parentElement;
*
* if (exclusive) {
* container.querySelectorAll(".accordion-item").forEach(i => {
* if (i !== item) toggleItem(i, false);
* });
* }
*
* toggleItem(item);
* });
*
* return {
* expandAll() {
* container.querySelectorAll(".accordion-item").forEach(item => {
* toggleItem(item, true);
* });
* },
*
* collapseAll() {
* container.querySelectorAll(".accordion-item").forEach(item => {
* toggleItem(item, false);
* });
* },
*
* toggle(index) {
* const items = container.querySelectorAll(".accordion-item");
* if (items[index]) toggleItem(items[index]);
* }
* };
* }
*/
// =============================================================
// EXERCISE 8: Tab Navigation
// =============================================================
/**
* Create tab navigation with delegation
*
* @param {string} containerId - ID of the tab container
* @param {Function} onChange - Callback when tab changes
* @returns {Object} - { activateTab(id), getActiveTab() }
*/
function createTabs(containerId, onChange) {
// Your code here
}
// Test
// const tabs = createTabs("product-tabs", (tabId) => console.log("Tab:", tabId));
// tabs.activateTab("reviews");
/*
* HINT: Handle tab button clicks, show/hide corresponding panels
*
* SOLUTION:
* function createTabs(containerId, onChange) {
* const container = document.getElementById(containerId);
* if (!container) return null;
*
* const tabList = container.querySelector(".tab-list");
* let activeTabId = null;
*
* function activateTab(tabId) {
* // Deactivate all tabs
* tabList.querySelectorAll(".tab").forEach(tab => {
* tab.classList.remove("active");
* tab.setAttribute("aria-selected", "false");
* });
*
* // Hide all panels
* container.querySelectorAll(".tab-panel").forEach(panel => {
* panel.classList.remove("active");
* panel.hidden = true;
* });
*
* // Activate selected tab
* const tab = tabList.querySelector(`[data-tab="${tabId}"]`);
* const panel = container.querySelector(`#${tabId}`);
*
* if (tab && panel) {
* tab.classList.add("active");
* tab.setAttribute("aria-selected", "true");
* panel.classList.add("active");
* panel.hidden = false;
* activeTabId = tabId;
*
* if (onChange) onChange(tabId);
* }
* }
*
* tabList.addEventListener("click", (event) => {
* const tab = event.target.closest("[data-tab]");
* if (tab) {
* activateTab(tab.dataset.tab);
* }
* });
*
* // Keyboard navigation
* tabList.addEventListener("keydown", (event) => {
* const tabs = [...tabList.querySelectorAll(".tab")];
* const currentIndex = tabs.findIndex(t => t.classList.contains("active"));
*
* if (event.key === "ArrowRight") {
* const nextIndex = (currentIndex + 1) % tabs.length;
* activateTab(tabs[nextIndex].dataset.tab);
* tabs[nextIndex].focus();
* } else if (event.key === "ArrowLeft") {
* const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
* activateTab(tabs[prevIndex].dataset.tab);
* tabs[prevIndex].focus();
* }
* });
*
* return {
* activateTab,
* getActiveTab() { return activeTabId; }
* };
* }
*/
// =============================================================
// EXERCISE 9: Dropdown Menu
// =============================================================
/**
* Create a dropdown menu with delegation
* Handle toggle, option selection, and outside clicks
*
* @param {string} containerId - ID of the dropdown container
* @param {Function} onSelect - Callback when option is selected
* @returns {Object} - { open(), close(), getSelected() }
*/
function createDropdown(containerId, onSelect) {
// Your code here
}
// Test
// const dropdown = createDropdown("country-select", (value, text) => {
// console.log("Selected:", value, text);
// });
/*
* HINT: Handle toggle click, option clicks, and document clicks for closing
*
* SOLUTION:
* function createDropdown(containerId, onSelect) {
* const container = document.getElementById(containerId);
* if (!container) return null;
*
* const toggle = container.querySelector(".dropdown-toggle");
* const menu = container.querySelector(".dropdown-menu");
* let selectedValue = null;
* let selectedText = null;
*
* function open() {
* container.classList.add("open");
* toggle.setAttribute("aria-expanded", "true");
* }
*
* function close() {
* container.classList.remove("open");
* toggle.setAttribute("aria-expanded", "false");
* }
*
* container.addEventListener("click", (event) => {
* if (event.target.closest(".dropdown-toggle")) {
* container.classList.toggle("open");
* }
*
* const option = event.target.closest(".dropdown-option");
* if (option) {
* selectedValue = option.dataset.value;
* selectedText = option.textContent;
*
* // Update toggle text
* toggle.textContent = selectedText;
*
* // Update active state
* menu.querySelectorAll(".dropdown-option").forEach(opt => {
* opt.classList.remove("active");
* });
* option.classList.add("active");
*
* close();
*
* if (onSelect) onSelect(selectedValue, selectedText);
* }
* });
*
* // Close on outside click
* document.addEventListener("click", (event) => {
* if (!container.contains(event.target)) {
* close();
* }
* });
*
* return {
* open,
* close,
* getSelected() { return { value: selectedValue, text: selectedText }; }
* };
* }
*/
// =============================================================
// EXERCISE 10: Tag Input
// =============================================================
/**
* Create a tag input component with delegation
* - Add tags on Enter
* - Remove tags on click
* - Prevent duplicates
*
* @param {string} containerId - ID of the tag input container
* @param {Object} options - { maxTags, allowDuplicates }
* @returns {Object} - { getTags(), addTag(text), removeTag(text), clear() }
*/
function createTagInput(containerId, options = {}) {
// Your code here
}
// Test
// const tags = createTagInput("tag-input", { maxTags: 5 });
// tags.addTag("javascript");
// tags.addTag("html");
/*
* HINT: Store tags in an array, delegate remove button clicks
*
* SOLUTION:
* function createTagInput(containerId, options = {}) {
* const container = document.getElementById(containerId);
* if (!container) return null;
*
* const { maxTags = Infinity, allowDuplicates = false } = options;
* const tagsContainer = container.querySelector(".tags") || container;
* const input = container.querySelector("input");
* const tags = [];
*
* function renderTags() {
* const tagElements = tagsContainer.querySelectorAll(".tag");
* tagElements.forEach(el => el.remove());
*
* tags.forEach(tag => {
* const tagEl = document.createElement("span");
* tagEl.className = "tag";
* tagEl.dataset.value = tag;
* tagEl.innerHTML = `${tag}<button class="tag-remove">×</button>`;
* tagsContainer.insertBefore(tagEl, input);
* });
* }
*
* function addTag(text) {
* const normalized = text.trim().toLowerCase();
* if (!normalized) return false;
* if (tags.length >= maxTags) return false;
* if (!allowDuplicates && tags.includes(normalized)) return false;
*
* tags.push(normalized);
* renderTags();
* return true;
* }
*
* function removeTag(text) {
* const index = tags.indexOf(text.toLowerCase());
* if (index > -1) {
* tags.splice(index, 1);
* renderTags();
* return true;
* }
* return false;
* }
*
* // Delegated remove handler
* tagsContainer.addEventListener("click", (event) => {
* if (event.target.classList.contains("tag-remove")) {
* const tag = event.target.parentElement;
* removeTag(tag.dataset.value);
* }
* });
*
* // Add on Enter
* if (input) {
* input.addEventListener("keydown", (event) => {
* if (event.key === "Enter") {
* event.preventDefault();
* addTag(input.value);
* input.value = "";
* }
* });
* }
*
* return {
* getTags() { return [...tags]; },
* addTag,
* removeTag,
* clear() {
* tags.length = 0;
* renderTags();
* }
* };
* }
*/
// =============================================================
// BONUS CHALLENGE: Data Table with CRUD
// =============================================================
/**
* Create a complete data table with CRUD operations
* All operations should use event delegation
*
* @param {string} tableId - ID of the table
* @param {Object} options - Configuration options
* @returns {Object} - { addRow(data), updateRow(id, data), deleteRow(id), getData() }
*
* Features:
* - Inline editing on cell double-click
* - Delete button per row
* - Select/deselect rows
* - Bulk operations
*/
function createDataTable(tableId, options = {}) {
// Your code here
}
/*
* SOLUTION:
* function createDataTable(tableId, options = {}) {
* const table = document.getElementById(tableId);
* if (!table) return null;
*
* const tbody = table.querySelector("tbody") || table;
* let nextId = 1;
* const data = new Map();
* const selectedRows = new Set();
*
* // Click handler for buttons
* tbody.addEventListener("click", (event) => {
* const row = event.target.closest("tr");
* if (!row) return;
*
* const rowId = row.dataset.id;
*
* if (event.target.matches(".delete-btn")) {
* deleteRow(rowId);
* } else if (event.target.matches(".select-checkbox")) {
* if (event.target.checked) {
* selectedRows.add(rowId);
* row.classList.add("selected");
* } else {
* selectedRows.delete(rowId);
* row.classList.remove("selected");
* }
* }
* });
*
* // Double-click for inline editing
* tbody.addEventListener("dblclick", (event) => {
* const cell = event.target.closest("td[data-field]");
* if (!cell) return;
*
* const row = cell.closest("tr");
* const field = cell.dataset.field;
* const currentValue = cell.textContent;
*
* const input = document.createElement("input");
* input.value = currentValue;
* input.className = "inline-edit";
* cell.textContent = "";
* cell.appendChild(input);
* input.focus();
* input.select();
*
* const save = () => {
* const newValue = input.value;
* cell.textContent = newValue;
*
* // Update data
* const rowData = data.get(row.dataset.id);
* if (rowData) {
* rowData[field] = newValue;
* }
* };
*
* input.addEventListener("blur", save);
* input.addEventListener("keydown", (e) => {
* if (e.key === "Enter") input.blur();
* if (e.key === "Escape") {
* input.value = currentValue;
* input.blur();
* }
* });
* });
*
* function addRow(rowData) {
* const id = String(nextId++);
* data.set(id, { ...rowData, id });
*
* const row = document.createElement("tr");
* row.dataset.id = id;
*
* const fields = options.columns || Object.keys(rowData);
* row.innerHTML = `
* <td><input type="checkbox" class="select-checkbox"></td>
* ${fields.map(f => `<td data-field="${f}">${rowData[f] || ""}</td>`).join("")}
* <td><button class="delete-btn">Delete</button></td>
* `;
*
* tbody.appendChild(row);
* return id;
* }
*
* function updateRow(id, newData) {
* const row = tbody.querySelector(`tr[data-id="${id}"]`);
* const rowData = data.get(id);
*
* if (row && rowData) {
* Object.assign(rowData, newData);
*
* Object.entries(newData).forEach(([field, value]) => {
* const cell = row.querySelector(`td[data-field="${field}"]`);
* if (cell) cell.textContent = value;
* });
*
* return true;
* }
* return false;
* }
*
* function deleteRow(id) {
* const row = tbody.querySelector(`tr[data-id="${id}"]`);
* if (row) {
* row.remove();
* data.delete(id);
* selectedRows.delete(id);
* return true;
* }
* return false;
* }
*
* return {
* addRow,
* updateRow,
* deleteRow,
* getData() {
* return Array.from(data.values());
* },
* getSelected() {
* return Array.from(selectedRows);
* },
* deleteSelected() {
* selectedRows.forEach(id => deleteRow(id));
* }
* };
* }
*/
// =============================================================
// Test HTML Template
// =============================================================
/*
Create an HTML file with this structure to test the exercises:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Event Delegation Exercises</title>
<style>
.active { background-color: #3498db; color: white; }
.completed { text-decoration: line-through; opacity: 0.6; }
.open > .accordion-content { max-height: 500px; }
.accordion-content { max-height: 0; overflow: hidden; transition: max-height 0.3s; }
.tab.active { border-bottom: 2px solid #3498db; }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
.dropdown.open .dropdown-menu { display: block; }
.dropdown-menu { display: none; }
.tag { display: inline-block; background: #e0e0e0; padding: 4px 8px; margin: 2px; border-radius: 4px; }
.tag-remove { border: none; background: none; cursor: pointer; margin-left: 4px; }
.selected { background-color: #e3f2fd; }
</style>
</head>
<body>
<ul id="myList">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<div id="options">
<button data-value="a">Option A</button>
<button data-value="b">Option B</button>
<button data-value="c">Option C</button>
</div>
<div id="cards">
<div class="card" data-id="1">
<div class="card-header">Card 1</div>
<div class="card-body">Content</div>
<div class="card-actions">
<button class="view-btn">View</button>
<button class="edit-btn">Edit</button>
<button class="delete-btn">Delete</button>
</div>
</div>
</div>
<div id="todo-app">
<input id="todo-input" placeholder="Add todo">
<button id="add-todo">Add</button>
<ul id="todo-list"></ul>
</div>
<div id="faq">
<div class="accordion-item">
<button class="accordion-header">Question 1</button>
<div class="accordion-content">Answer 1</div>
</div>
<div class="accordion-item">
<button class="accordion-header">Question 2</button>
<div class="accordion-content">Answer 2</div>
</div>
</div>
<div id="product-tabs">
<div class="tab-list">
<button class="tab active" data-tab="description">Description</button>
<button class="tab" data-tab="reviews">Reviews</button>
</div>
<div id="description" class="tab-panel active">Description content</div>
<div id="reviews" class="tab-panel">Reviews content</div>
</div>
<div id="country-select" class="dropdown">
<button class="dropdown-toggle">Select Country</button>
<div class="dropdown-menu">
<div class="dropdown-option" data-value="us">United States</div>
<div class="dropdown-option" data-value="uk">United Kingdom</div>
<div class="dropdown-option" data-value="ca">Canada</div>
</div>
</div>
<div id="tag-input">
<div class="tags">
<input type="text" placeholder="Add tag">
</div>
</div>
<script src="exercises.js"></script>
</body>
</html>
*/
// =============================================================
// Summary
// =============================================================
console.log('='.repeat(60));
console.log('10.4 Event Delegation - Exercises Loaded');
console.log('='.repeat(60));
console.log('Exercises 1-3: Basic delegation patterns');
console.log('Exercises 4-5: Dynamic content and action routing');
console.log('Exercises 6-7: Tables and accordions');
console.log('Exercises 8-10: Tabs, dropdowns, tag inputs');
console.log('Bonus: Full CRUD data table');
console.log('='.repeat(60));