javascript

exercises

exercises.js
/**
 * ============================================================
 * 10.2 Modifying the DOM - Exercises
 * ============================================================
 *
 * Practice modifying DOM elements including content,
 * attributes, classes, and styles.
 *
 * Instructions:
 * 1. Read each exercise description carefully
 * 2. Write your solution in the provided function
 * 3. Test with the example HTML provided
 * 4. Check the hints and solutions if needed
 */

// =============================================================
// EXERCISE 1: Update Text Content
// =============================================================
/**
 * Update the text content of an element safely (no HTML parsing)
 *
 * @param {string} elementId - The ID of the element to update
 * @param {string} newText - The new text content
 * @returns {boolean} - True if successful, false if element not found
 *
 * Example HTML: <p id="greeting">Hello</p>
 * updateText("greeting", "Welcome!") → element shows "Welcome!"
 */
function updateText(elementId, newText) {
  // Your code here
}

// Test
// console.log(updateText("greeting", "Welcome, User!"));

/*
 * HINT: Use textContent to safely set text without HTML parsing
 *
 * SOLUTION:
 * function updateText(elementId, newText) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return false;
 *     element.textContent = newText;
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 2: Safe HTML Insertion
// =============================================================
/**
 * Insert HTML content with basic sanitization
 * Remove any <script> tags from the content before inserting
 *
 * @param {string} elementId - The ID of the target element
 * @param {string} htmlContent - The HTML content to insert
 * @returns {boolean} - True if successful
 *
 * Example:
 * safeInsertHTML("container", "<p>Hello</p><script>evil()</script>")
 * → Only inserts "<p>Hello</p>"
 */
function safeInsertHTML(elementId, htmlContent) {
  // Your code here
}

// Test
// safeInsertHTML("container", "<p>Safe content</p><script>alert('xss')</script>");

/*
 * HINT: Use regex to remove script tags: /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi
 *
 * SOLUTION:
 * function safeInsertHTML(elementId, htmlContent) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return false;
 *
 *     // Remove script tags
 *     const sanitized = htmlContent.replace(
 *         /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
 *         ""
 *     );
 *
 *     element.innerHTML = sanitized;
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 3: Toggle Attribute
// =============================================================
/**
 * Toggle an attribute on an element (add if missing, remove if present)
 *
 * @param {string} elementId - The ID of the element
 * @param {string} attributeName - The name of the attribute to toggle
 * @param {string} attributeValue - The value to set when adding (default: "")
 * @returns {boolean} - True if attribute is now present, false if removed
 *
 * Example HTML: <button id="btn">Click</button>
 * toggleAttribute("btn", "disabled") → adds disabled, returns true
 * toggleAttribute("btn", "disabled") → removes disabled, returns false
 */
function toggleAttribute(elementId, attributeName, attributeValue = '') {
  // Your code here
}

// Test
// console.log(toggleAttribute("submitBtn", "disabled"));

/*
 * HINT: Use hasAttribute to check, then setAttribute or removeAttribute
 *
 * SOLUTION:
 * function toggleAttribute(elementId, attributeName, attributeValue = "") {
 *     const element = document.getElementById(elementId);
 *     if (!element) return null;
 *
 *     if (element.hasAttribute(attributeName)) {
 *         element.removeAttribute(attributeName);
 *         return false;
 *     } else {
 *         element.setAttribute(attributeName, attributeValue);
 *         return true;
 *     }
 * }
 */

// =============================================================
// EXERCISE 4: Set Multiple Attributes
// =============================================================
/**
 * Set multiple attributes on an element at once
 *
 * @param {string} elementId - The ID of the element
 * @param {Object} attributes - Object with attribute name-value pairs
 * @returns {boolean} - True if successful
 *
 * Example:
 * setAttributes("myImg", {
 *     src: "photo.jpg",
 *     alt: "A photo",
 *     width: "300",
 *     height: "200"
 * });
 */
function setAttributes(elementId, attributes) {
  // Your code here
}

// Test
// setAttributes("testImg", { src: "test.jpg", alt: "Test", loading: "lazy" });

/*
 * HINT: Use Object.entries() to iterate over the attributes object
 *
 * SOLUTION:
 * function setAttributes(elementId, attributes) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return false;
 *
 *     Object.entries(attributes).forEach(([name, value]) => {
 *         element.setAttribute(name, value);
 *     });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 5: Class Toggle with State
// =============================================================
/**
 * Toggle a class and return the new state
 * Also update a data attribute to track the state
 *
 * @param {string} elementId - The ID of the element
 * @param {string} className - The class to toggle
 * @returns {Object} - { hasClass: boolean, element: Element }
 *
 * Example:
 * toggleClassWithState("menu", "open")
 * → { hasClass: true, element: <element> } if class was added
 */
function toggleClassWithState(elementId, className) {
  // Your code here
}

// Test
// console.log(toggleClassWithState("sidebar", "collapsed"));

/*
 * HINT: Use classList.toggle() which returns the new state
 *
 * SOLUTION:
 * function toggleClassWithState(elementId, className) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return null;
 *
 *     const hasClass = element.classList.toggle(className);
 *     element.dataset[className] = hasClass;
 *
 *     return { hasClass, element };
 * }
 */

// =============================================================
// EXERCISE 6: Add/Remove Multiple Classes
// =============================================================
/**
 * Manage multiple classes on an element
 *
 * @param {string} elementId - The ID of the element
 * @param {Object} classChanges - { add: [...classes], remove: [...classes] }
 * @returns {string[]} - Array of current classes after changes
 *
 * Example:
 * manageClasses("box", {
 *     add: ["active", "highlighted"],
 *     remove: ["hidden", "disabled"]
 * });
 */
function manageClasses(elementId, classChanges) {
  // Your code here
}

// Test
// console.log(manageClasses("card", { add: ["active", "animated"], remove: ["hidden"] }));

/*
 * HINT: Use classList.add() and classList.remove() with spread operator
 *
 * SOLUTION:
 * function manageClasses(elementId, classChanges) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return null;
 *
 *     if (classChanges.add && classChanges.add.length) {
 *         element.classList.add(...classChanges.add);
 *     }
 *
 *     if (classChanges.remove && classChanges.remove.length) {
 *         element.classList.remove(...classChanges.remove);
 *     }
 *
 *     return Array.from(element.classList);
 * }
 */

// =============================================================
// EXERCISE 7: Switch Active Item
// =============================================================
/**
 * Switch the "active" class to a specific item among siblings
 * Remove "active" from all items, add to the specified one
 *
 * @param {string} containerId - The ID of the container
 * @param {string} activeItemSelector - Selector for the item to make active
 * @returns {Element|null} - The newly active element
 *
 * Example HTML:
 * <ul id="menu">
 *   <li class="active">Home</li>
 *   <li>About</li>
 *   <li>Contact</li>
 * </ul>
 *
 * switchActive("menu", "li:nth-child(2)") → About becomes active
 */
function switchActive(containerId, activeItemSelector) {
  // Your code here
}

// Test
// switchActive("tabList", "[data-tab='settings']");

/*
 * HINT: Get all children, remove active from all, add to selected
 *
 * SOLUTION:
 * function switchActive(containerId, activeItemSelector) {
 *     const container = document.getElementById(containerId);
 *     if (!container) return null;
 *
 *     // Remove active from all children
 *     container.querySelectorAll(".active").forEach(item => {
 *         item.classList.remove("active");
 *     });
 *
 *     // Add active to selected item
 *     const activeItem = container.querySelector(activeItemSelector);
 *     if (activeItem) {
 *         activeItem.classList.add("active");
 *     }
 *
 *     return activeItem;
 * }
 */

// =============================================================
// EXERCISE 8: Apply Style Object
// =============================================================
/**
 * Apply a style object to an element
 * Support both camelCase and kebab-case property names
 *
 * @param {string} elementId - The ID of the element
 * @param {Object} styles - Object with CSS property-value pairs
 * @returns {boolean} - True if successful
 *
 * Example:
 * applyStyles("box", {
 *     "background-color": "blue",
 *     fontSize: "16px",
 *     "border-radius": "8px"
 * });
 */
function applyStyles(elementId, styles) {
  // Your code here
}

// Test
// applyStyles("testBox", { backgroundColor: "red", padding: "20px" });

/*
 * HINT: Convert kebab-case to camelCase, then set on style object
 *
 * SOLUTION:
 * function applyStyles(elementId, styles) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return false;
 *
 *     Object.entries(styles).forEach(([prop, value]) => {
 *         // Convert kebab-case to camelCase
 *         const camelProp = prop.replace(/-([a-z])/g, (_, char) =>
 *             char.toUpperCase()
 *         );
 *         element.style[camelProp] = value;
 *     });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 9: Reset Styles
// =============================================================
/**
 * Remove specific inline styles from an element
 *
 * @param {string} elementId - The ID of the element
 * @param {string[]} styleProperties - Array of CSS properties to remove
 * @returns {boolean} - True if successful
 *
 * Example:
 * resetStyles("box", ["backgroundColor", "padding", "margin"]);
 */
function resetStyles(elementId, styleProperties) {
  // Your code here
}

// Test
// resetStyles("styled", ["color", "fontSize"]);

/*
 * HINT: Use style.removeProperty() with kebab-case names
 *
 * SOLUTION:
 * function resetStyles(elementId, styleProperties) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return false;
 *
 *     styleProperties.forEach(prop => {
 *         // Convert camelCase to kebab-case
 *         const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
 *         element.style.removeProperty(kebabProp);
 *     });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 10: Read Data Attributes
// =============================================================
/**
 * Read all data attributes from an element and return as object
 * Also convert numeric strings to numbers
 *
 * @param {string} elementId - The ID of the element
 * @returns {Object|null} - Object with all data attribute values
 *
 * Example HTML: <div id="config" data-limit="10" data-enabled="true" data-name="test">
 * getDataAttributes("config") → { limit: 10, enabled: "true", name: "test" }
 */
function getDataAttributes(elementId) {
  // Your code here
}

// Test
// console.log(getDataAttributes("productCard"));

/*
 * HINT: Use element.dataset and convert numeric strings
 *
 * SOLUTION:
 * function getDataAttributes(elementId) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return null;
 *
 *     const result = {};
 *
 *     Object.entries(element.dataset).forEach(([key, value]) => {
 *         // Try to convert to number if it looks like one
 *         const num = Number(value);
 *         result[key] = !isNaN(num) && value !== "" ? num : value;
 *     });
 *
 *     return result;
 * }
 */

// =============================================================
// EXERCISE 11: Set Data Attributes
// =============================================================
/**
 * Set multiple data attributes on an element
 * Handle objects by JSON stringifying them
 *
 * @param {string} elementId - The ID of the element
 * @param {Object} dataObj - Object with data attribute name-value pairs
 * @returns {boolean} - True if successful
 *
 * Example:
 * setDataAttributes("item", {
 *     id: 123,
 *     config: { theme: "dark", size: "large" }
 * });
 * → data-id="123", data-config='{"theme":"dark","size":"large"}'
 */
function setDataAttributes(elementId, dataObj) {
  // Your code here
}

// Test
// setDataAttributes("widget", { id: 1, settings: { enabled: true } });

/*
 * HINT: Use typeof to check for objects and JSON.stringify for them
 *
 * SOLUTION:
 * function setDataAttributes(elementId, dataObj) {
 *     const element = document.getElementById(elementId);
 *     if (!element) return false;
 *
 *     Object.entries(dataObj).forEach(([key, value]) => {
 *         if (typeof value === "object") {
 *             element.dataset[key] = JSON.stringify(value);
 *         } else {
 *             element.dataset[key] = value;
 *         }
 *     });
 *
 *     return true;
 * }
 */

// =============================================================
// EXERCISE 12: Create Styled Element
// =============================================================
/**
 * Create a styled element with specified tag, content, classes, and styles
 *
 * @param {Object} config - Configuration object
 * @param {string} config.tag - HTML tag name
 * @param {string} config.content - Text content
 * @param {string[]} config.classes - Array of class names
 * @param {Object} config.styles - Style properties
 * @param {Object} config.attributes - Other attributes
 * @returns {Element} - The created element
 *
 * Example:
 * createStyledElement({
 *     tag: "button",
 *     content: "Click Me",
 *     classes: ["btn", "btn-primary"],
 *     styles: { padding: "10px 20px" },
 *     attributes: { type: "button", disabled: "" }
 * });
 */
function createStyledElement(config) {
  // Your code here
}

// Test
// const btn = createStyledElement({ tag: "span", content: "Badge", classes: ["badge"] });

/*
 * HINT: Use document.createElement, then apply all configurations
 *
 * SOLUTION:
 * function createStyledElement(config) {
 *     const { tag, content, classes = [], styles = {}, attributes = {} } = config;
 *
 *     const element = document.createElement(tag);
 *
 *     if (content) {
 *         element.textContent = content;
 *     }
 *
 *     if (classes.length) {
 *         element.classList.add(...classes);
 *     }
 *
 *     Object.assign(element.style, styles);
 *
 *     Object.entries(attributes).forEach(([name, value]) => {
 *         element.setAttribute(name, value);
 *     });
 *
 *     return element;
 * }
 */

// =============================================================
// EXERCISE 13: Build Card Component
// =============================================================
/**
 * Build a card component with header, body, and footer
 *
 * @param {Object} cardData - Card data
 * @param {string} cardData.title - Card title
 * @param {string} cardData.content - Card body content
 * @param {string} cardData.footer - Card footer text
 * @param {string} cardData.theme - Theme class (optional)
 * @returns {Element} - The card element
 *
 * Expected structure:
 * <div class="card [theme]">
 *   <div class="card-header">[title]</div>
 *   <div class="card-body">[content]</div>
 *   <div class="card-footer">[footer]</div>
 * </div>
 */
function buildCard(cardData) {
  // Your code here
}

// Test
// const card = buildCard({ title: "Hello", content: "World", footer: "2024" });

/*
 * HINT: Create container and children, use innerHTML or create elements
 *
 * SOLUTION:
 * function buildCard(cardData) {
 *     const { title, content, footer, theme } = cardData;
 *
 *     const card = document.createElement("div");
 *     card.className = theme ? `card ${theme}` : "card";
 *
 *     if (title) {
 *         const header = document.createElement("div");
 *         header.className = "card-header";
 *         header.textContent = title;
 *         card.appendChild(header);
 *     }
 *
 *     if (content) {
 *         const body = document.createElement("div");
 *         body.className = "card-body";
 *         body.textContent = content;
 *         card.appendChild(body);
 *     }
 *
 *     if (footer) {
 *         const foot = document.createElement("div");
 *         foot.className = "card-footer";
 *         foot.textContent = footer;
 *         card.appendChild(foot);
 *     }
 *
 *     return card;
 * }
 */

// =============================================================
// EXERCISE 14: Style Highlighter
// =============================================================
/**
 * Highlight an element temporarily by adding styles/classes
 * Then remove the highlight after a delay
 *
 * @param {string} elementId - The ID of the element
 * @param {Object} options - Highlight options
 * @param {string} options.color - Background color for highlight
 * @param {number} options.duration - Duration in ms (default: 2000)
 * @returns {Promise<void>} - Resolves when highlight is removed
 *
 * Example:
 * await highlightElement("result", { color: "yellow", duration: 1500 });
 */
function highlightElement(elementId, options = {}) {
  // Your code here
}

// Test
// highlightElement("message", { color: "#ffeb3b", duration: 2000 });

/*
 * HINT: Store original style, apply highlight, use setTimeout to restore
 *
 * SOLUTION:
 * function highlightElement(elementId, options = {}) {
 *     return new Promise((resolve) => {
 *         const element = document.getElementById(elementId);
 *         if (!element) {
 *             resolve();
 *             return;
 *         }
 *
 *         const { color = "yellow", duration = 2000 } = options;
 *
 *         // Store original background
 *         const originalBg = element.style.backgroundColor;
 *         const originalTransition = element.style.transition;
 *
 *         // Apply highlight
 *         element.style.transition = "background-color 0.3s";
 *         element.style.backgroundColor = color;
 *
 *         // Remove highlight after duration
 *         setTimeout(() => {
 *             element.style.backgroundColor = originalBg;
 *
 *             // Cleanup transition after animation
 *             setTimeout(() => {
 *                 element.style.transition = originalTransition;
 *                 resolve();
 *             }, 300);
 *         }, duration);
 *     });
 * }
 */

// =============================================================
// EXERCISE 15: Theme Switcher
// =============================================================
/**
 * Create a theme switcher that:
 * - Toggles between light and dark themes
 * - Updates CSS variables
 * - Saves preference to localStorage
 * - Applies to document root
 *
 * @param {Object} themes - Theme definitions
 * @param {Object} themes.light - Light theme CSS variables
 * @param {Object} themes.dark - Dark theme CSS variables
 * @returns {Object} - Switcher API { toggle, setTheme, getTheme }
 *
 * Example:
 * const switcher = createThemeSwitcher({
 *     light: { "--bg-color": "#fff", "--text-color": "#333" },
 *     dark: { "--bg-color": "#333", "--text-color": "#fff" }
 * });
 * switcher.toggle();
 */
function createThemeSwitcher(themes) {
  // Your code here
}

// Test
// const switcher = createThemeSwitcher({
//     light: { "--bg": "white" },
//     dark: { "--bg": "black" }
// });
// switcher.toggle();

/*
 * HINT: Use document.documentElement.style.setProperty for CSS variables
 *
 * SOLUTION:
 * function createThemeSwitcher(themes) {
 *     const root = document.documentElement;
 *     let currentTheme = localStorage.getItem("theme") || "light";
 *
 *     function applyTheme(themeName) {
 *         const theme = themes[themeName];
 *         if (!theme) return;
 *
 *         Object.entries(theme).forEach(([prop, value]) => {
 *             root.style.setProperty(prop, value);
 *         });
 *
 *         root.dataset.theme = themeName;
 *         localStorage.setItem("theme", themeName);
 *         currentTheme = themeName;
 *     }
 *
 *     // Apply initial theme
 *     applyTheme(currentTheme);
 *
 *     return {
 *         toggle() {
 *             const newTheme = currentTheme === "light" ? "dark" : "light";
 *             applyTheme(newTheme);
 *             return newTheme;
 *         },
 *
 *         setTheme(themeName) {
 *             applyTheme(themeName);
 *             return themeName;
 *         },
 *
 *         getTheme() {
 *             return currentTheme;
 *         }
 *     };
 * }
 */

// =============================================================
// BONUS CHALLENGE: DOM State Manager
// =============================================================
/**
 * Create a state manager that syncs JavaScript state with DOM
 *
 * Features:
 * - Store state values
 * - Automatically update DOM elements when state changes
 * - Support text, attribute, class, and style bindings
 *
 * @param {Object} initialState - Initial state values
 * @param {Object} bindings - DOM bindings { stateProp: { type, selector, ... } }
 * @returns {Object} - State manager API
 *
 * Binding types:
 * - text: { type: "text", selector: "#el" }
 * - attribute: { type: "attribute", selector: "#el", attribute: "href" }
 * - class: { type: "class", selector: "#el", className: "active" }
 * - style: { type: "style", selector: "#el", property: "display" }
 *
 * Example:
 * const state = createDOMStateManager(
 *     { count: 0, visible: true },
 *     {
 *         count: { type: "text", selector: "#counter" },
 *         visible: { type: "class", selector: "#panel", className: "visible" }
 *     }
 * );
 * state.set("count", 5); // Updates #counter text to "5"
 */
function createDOMStateManager(initialState, bindings) {
  // Your code here
}

/*
 * SOLUTION:
 * function createDOMStateManager(initialState, bindings) {
 *     const state = { ...initialState };
 *
 *     function updateDOM(prop, value) {
 *         const binding = bindings[prop];
 *         if (!binding) return;
 *
 *         const element = document.querySelector(binding.selector);
 *         if (!element) return;
 *
 *         switch (binding.type) {
 *             case "text":
 *                 element.textContent = value;
 *                 break;
 *             case "attribute":
 *                 element.setAttribute(binding.attribute, value);
 *                 break;
 *             case "class":
 *                 element.classList.toggle(binding.className, !!value);
 *                 break;
 *             case "style":
 *                 element.style[binding.property] = value;
 *                 break;
 *         }
 *     }
 *
 *     // Apply initial state
 *     Object.keys(state).forEach(prop => {
 *         updateDOM(prop, state[prop]);
 *     });
 *
 *     return {
 *         get(prop) {
 *             return state[prop];
 *         },
 *
 *         set(prop, value) {
 *             state[prop] = value;
 *             updateDOM(prop, value);
 *             return value;
 *         },
 *
 *         getAll() {
 *             return { ...state };
 *         },
 *
 *         setAll(newState) {
 *             Object.entries(newState).forEach(([prop, value]) => {
 *                 state[prop] = value;
 *                 updateDOM(prop, value);
 *             });
 *         }
 *     };
 * }
 */

// =============================================================
// 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>DOM Modification Exercises</title>
    <style>
        .active { background-color: #3498db; color: white; }
        .highlight { border: 2px solid yellow; }
        .hidden { display: none; }
        .visible { display: block; }
        .card { border: 1px solid #ddd; border-radius: 8px; margin: 10px; }
        .card-header { background: #f5f5f5; padding: 10px; font-weight: bold; }
        .card-body { padding: 15px; }
        .card-footer { background: #f5f5f5; padding: 10px; font-size: 0.9em; }
    </style>
</head>
<body>
    <p id="greeting">Hello World</p>
    <div id="container"></div>
    <button id="btn">Test Button</button>
    <img id="testImg" src="placeholder.jpg" alt="Placeholder">
    <div id="box" class="box"></div>
    <ul id="menu">
        <li class="active">Home</li>
        <li>About</li>
        <li>Contact</li>
    </ul>
    <div id="testBox" style="width: 100px; height: 100px; background: gray;"></div>
    <div id="productCard" data-id="123" data-price="99.99" data-category="electronics"></div>
    <div id="widget"></div>
    <div id="message">Test message</div>
    <div id="cards"></div>

    <script src="exercises.js"></script>
</body>
</html>
*/

// =============================================================
// Summary
// =============================================================
console.log('='.repeat(60));
console.log('10.2 Modifying the DOM - Exercises Loaded');
console.log('='.repeat(60));
console.log('Exercises 1-3:   Content modification (text, HTML, attributes)');
console.log('Exercises 4-5:   Attribute manipulation');
console.log('Exercises 6-7:   Class manipulation');
console.log('Exercises 8-9:   Style manipulation');
console.log('Exercises 10-11: Data attributes');
console.log('Exercises 12-13: Component building');
console.log('Exercises 14-15: Practical utilities');
console.log('Bonus:           DOM State Manager');
console.log('='.repeat(60));
Exercises - JavaScript Tutorial | DeepML