javascript

exercises

exercises.js
/**
 * Component Patterns - Exercises
 *
 * Practice implementing advanced Web Component patterns
 */

// ============================================
// EXERCISE 1: Event-Driven Component
// ============================================

/**
 * Exercise: Create a NotificationManager component
 *
 * Requirements:
 * 1. Uses event-driven architecture
 * 2. Can show, hide, and auto-dismiss notifications
 * 3. Supports different notification types (success, error, warning, info)
 * 4. Queues notifications if multiple arrive at once
 * 5. Provides smooth animations
 *
 * Events:
 * - 'notification-shown': Fired when notification appears
 * - 'notification-hidden': Fired when notification is dismissed
 */

class NotificationManager extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // TODO: Initialize notification queue
    // TODO: Set up default options (duration, position, etc.)
  }

  connectedCallback() {
    // TODO: Render the container
    // TODO: Set up event listener for 'add-notification' events
  }

  show(message, type = 'info', duration = 3000) {
    // TODO: Create notification element
    // TODO: Add to queue or display immediately
    // TODO: Set up auto-dismiss timer
    // TODO: Dispatch 'notification-shown' event
  }

  hide(notification) {
    // TODO: Animate out
    // TODO: Remove from DOM
    // TODO: Process queue
    // TODO: Dispatch 'notification-hidden' event
  }

  _processQueue() {
    // TODO: Check if can show more notifications
    // TODO: Dequeue and show next notification
  }

  render() {
    this.shadowRoot.innerHTML = `
            <style>
                /* TODO: Style notification container */
                /* TODO: Style individual notifications */
                /* TODO: Add type-based color variations */
                /* TODO: Add entrance/exit animations */
            </style>
            <div class="notifications"></div>
        `;
  }
}

// TODO: Define custom element
// customElements.define('notification-manager', NotificationManager);

// ============================================
// EXERCISE 2: Observable State Component
// ============================================

/**
 * Exercise: Create a TodoApp with observable state
 *
 * Requirements:
 * 1. Uses Proxy-based reactive state
 * 2. Supports add, toggle, remove, and filter todos
 * 3. Persists state to localStorage
 * 4. Batches DOM updates
 * 5. Provides undo/redo functionality
 *
 * State shape:
 * {
 *   todos: [{ id, text, completed, createdAt }],
 *   filter: 'all' | 'active' | 'completed'
 * }
 */

function createObservableState(initialState, onChange) {
  // TODO: Create deep reactive Proxy
  // TODO: Handle nested objects
  // TODO: Track changes for undo/redo

  return null; // Replace with Proxy
}

class TodoApp extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // TODO: Initialize observable state
    // TODO: Set up undo/redo history stacks
  }

  connectedCallback() {
    // TODO: Load from localStorage
    // TODO: Render initial state
  }

  addTodo(text) {
    // TODO: Add todo to state
    // TODO: Save to localStorage
  }

  toggleTodo(id) {
    // TODO: Toggle completed status
    // TODO: Save to localStorage
  }

  removeTodo(id) {
    // TODO: Remove todo from state
    // TODO: Save to localStorage
  }

  setFilter(filter) {
    // TODO: Update filter in state
  }

  undo() {
    // TODO: Pop from history
    // TODO: Restore previous state
  }

  redo() {
    // TODO: Pop from redo stack
    // TODO: Apply state
  }

  get filteredTodos() {
    // TODO: Return filtered todos based on current filter
  }

  render() {
    // TODO: Implement render with filter tabs, input, and todo list
  }
}

// TODO: Define custom element
// customElements.define('todo-app', TodoApp);

// ============================================
// EXERCISE 3: Composition Pattern Component
// ============================================

/**
 * Exercise: Create a DataGrid component using composition
 *
 * Requirements:
 * 1. Composed of smaller components: GridHeader, GridRow, GridCell
 * 2. Supports sorting by clicking headers
 * 3. Supports row selection (single and multiple)
 * 4. Supports inline editing
 * 5. Uses slots for custom cell renderers
 *
 * Usage example:
 * <data-grid>
 *   <grid-column field="name" header="Name" sortable></grid-column>
 *   <grid-column field="age" header="Age" type="number"></grid-column>
 * </data-grid>
 */

class GridColumn extends HTMLElement {
  static get observedAttributes() {
    return ['field', 'header', 'sortable', 'type', 'editable'];
  }

  // TODO: Implement attribute getters
}

class DataGrid extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // TODO: Initialize data, sorting, selection state
  }

  set data(value) {
    // TODO: Store data
    // TODO: Trigger render
  }

  get data() {
    // TODO: Return current data
  }

  get selectedRows() {
    // TODO: Return selected row data
  }

  connectedCallback() {
    // TODO: Get column definitions from child elements
    // TODO: Render grid
  }

  sort(field) {
    // TODO: Toggle sort direction
    // TODO: Sort data
    // TODO: Update header indicators
    // TODO: Re-render rows
  }

  selectRow(index, addToSelection = false) {
    // TODO: Handle single/multiple selection
    // TODO: Dispatch 'selection-change' event
  }

  startEdit(rowIndex, field) {
    // TODO: Replace cell content with input
    // TODO: Handle save/cancel
  }

  _renderHeader() {
    // TODO: Create header row with sortable columns
  }

  _renderRows() {
    // TODO: Create data rows with cells
  }

  render() {
    this.shadowRoot.innerHTML = `
            <style>
                /* TODO: Style grid table */
                /* TODO: Style sortable headers */
                /* TODO: Style selected rows */
                /* TODO: Style editable cells */
            </style>
            <table>
                <thead></thead>
                <tbody></tbody>
            </table>
        `;

    // TODO: Call _renderHeader and _renderRows
  }
}

// TODO: Define custom elements
// customElements.define('grid-column', GridColumn);
// customElements.define('data-grid', DataGrid);

// ============================================
// EXERCISE 4: Plugin Architecture Component
// ============================================

/**
 * Exercise: Create a TextEditor with plugin support
 *
 * Requirements:
 * 1. Base editor supports basic text editing
 * 2. Plugins can add toolbar buttons
 * 3. Plugins can modify content (format, transform)
 * 4. Plugins can hook into events (beforeInput, afterChange)
 * 5. Plugins can be registered and unregistered dynamically
 *
 * Plugin interface:
 * {
 *   name: string,
 *   init: (editor) => void,
 *   destroy: () => void,
 *   commands: { [name]: handler },
 *   toolbar: { icon, tooltip, action }
 * }
 */

class TextEditor extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // TODO: Initialize plugins Map
    // TODO: Initialize event hooks
  }

  static get observedAttributes() {
    return ['placeholder', 'readonly'];
  }

  get value() {
    // TODO: Return editor content
  }

  set value(content) {
    // TODO: Set editor content
    // TODO: Notify plugins of change
  }

  connectedCallback() {
    // TODO: Render editor
    // TODO: Set up input listeners
  }

  registerPlugin(plugin) {
    // TODO: Store plugin
    // TODO: Call plugin.init with editor reference
    // TODO: Add toolbar buttons if defined
    // TODO: Register commands
  }

  unregisterPlugin(pluginName) {
    // TODO: Call plugin.destroy
    // TODO: Remove toolbar buttons
    // TODO: Unregister commands
  }

  executeCommand(commandName, ...args) {
    // TODO: Find command from plugins
    // TODO: Execute command
  }

  addHook(hookName, handler) {
    // TODO: Add event hook
  }

  removeHook(hookName, handler) {
    // TODO: Remove event hook
  }

  _runHooks(hookName, data) {
    // TODO: Run all hooks for event
    // TODO: Allow hooks to cancel/modify
  }

  render() {
    this.shadowRoot.innerHTML = `
            <style>
                /* TODO: Style editor container */
                /* TODO: Style toolbar */
                /* TODO: Style content area */
            </style>
            <div class="toolbar"></div>
            <div class="content" contenteditable="true"></div>
        `;
  }
}

// Example plugin
const BoldPlugin = {
  name: 'bold',

  init(editor) {
    this.editor = editor;
  },

  destroy() {
    this.editor = null;
  },

  commands: {
    bold: (editor) => {
      document.execCommand('bold');
    },
  },

  toolbar: {
    icon: 'B',
    tooltip: 'Bold (Ctrl+B)',
    action: 'bold',
  },
};

// TODO: Define custom element
// customElements.define('text-editor', TextEditor);

// ============================================
// EXERCISE 5: Virtual Scroll Component
// ============================================

/**
 * Exercise: Create a VirtualList for large datasets
 *
 * Requirements:
 * 1. Only renders visible items + buffer
 * 2. Supports variable height items
 * 3. Smooth scrolling experience
 * 4. Handles dynamic data updates
 * 5. Provides scroll position restoration
 *
 * Properties:
 * - items: Array of data
 * - itemHeight: Fixed or function(index) => height
 * - buffer: Number of extra items to render
 * - renderItem: Function(item, index) => HTML
 */

class VirtualList extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });

    // TODO: Initialize state
    this._items = [];
    this._itemHeight = 50;
    this._buffer = 5;
    this._renderItem = null;
    this._scrollTop = 0;
    this._visibleStart = 0;
    this._visibleEnd = 0;
  }

  set items(value) {
    // TODO: Store items
    // TODO: Calculate total height
    // TODO: Update visible range
    // TODO: Render
  }

  set itemHeight(value) {
    // TODO: Store item height (number or function)
  }

  set renderItem(fn) {
    // TODO: Store render function
  }

  connectedCallback() {
    // TODO: Render container
    // TODO: Set up scroll listener with throttling
    // TODO: Set up resize observer
  }

  disconnectedCallback() {
    // TODO: Clean up observers and listeners
  }

  _calculateVisibleRange() {
    // TODO: Calculate start and end indices based on scroll position
    // TODO: Include buffer items
  }

  _getItemHeight(index) {
    // TODO: Return height for item at index
  }

  _getItemTop(index) {
    // TODO: Calculate top position for item
  }

  _updateVisibleItems() {
    // TODO: Remove items outside visible range
    // TODO: Add newly visible items
    // TODO: Position items absolutely
  }

  scrollToIndex(index, behavior = 'smooth') {
    // TODO: Calculate scroll position for index
    // TODO: Scroll to position
  }

  render() {
    this.shadowRoot.innerHTML = `
            <style>
                :host {
                    display: block;
                    overflow: auto;
                    position: relative;
                }
                .scroll-content {
                    position: relative;
                }
                .item {
                    position: absolute;
                    left: 0;
                    right: 0;
                }
            </style>
            <div class="scroll-content">
                <!-- Virtual items rendered here -->
            </div>
        `;
  }
}

// TODO: Define custom element
// customElements.define('virtual-list', VirtualList);

// ============================================
// EXERCISE 6: Context Provider/Consumer Pattern
// ============================================

/**
 * Exercise: Create a theme context system
 *
 * Requirements:
 * 1. ThemeProvider sets theme for all children
 * 2. ThemeConsumer components receive theme automatically
 * 3. Nested providers override parent theme
 * 4. Theme changes propagate to all consumers
 * 5. Fallback theme for consumers without provider
 *
 * Usage:
 * <theme-provider theme="dark">
 *   <theme-consumer>...</theme-consumer>
 * </theme-provider>
 */

const ThemeContext = {
  providers: new WeakMap(),
  consumers: new WeakMap(),

  getTheme(consumer) {
    // TODO: Walk up DOM tree to find provider
    // TODO: Return provider's theme or default
  },

  registerProvider(element, theme) {
    // TODO: Store provider
    // TODO: Notify descendant consumers
  },

  registerConsumer(element, callback) {
    // TODO: Store consumer with callback
    // TODO: Get initial theme and call callback
  },

  unregisterProvider(element) {
    // TODO: Clean up provider
  },

  unregisterConsumer(element) {
    // TODO: Clean up consumer
  },
};

class ThemeProvider extends HTMLElement {
  static get observedAttributes() {
    return ['theme'];
  }

  connectedCallback() {
    // TODO: Register with ThemeContext
  }

  disconnectedCallback() {
    // TODO: Unregister from ThemeContext
  }

  attributeChangedCallback(name, oldVal, newVal) {
    // TODO: Notify ThemeContext of change
  }
}

class ThemeConsumer extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    // TODO: Register with ThemeContext
    // TODO: Provide callback for theme updates
  }

  disconnectedCallback() {
    // TODO: Unregister from ThemeContext
  }

  _onThemeChange(theme) {
    // TODO: Update component based on theme
  }

  render(theme) {
    // TODO: Render with theme-based styles
  }
}

// TODO: Define custom elements
// customElements.define('theme-provider', ThemeProvider);
// customElements.define('theme-consumer', ThemeConsumer);

// ============================================
// EXERCISE 7: Test Your Components
// ============================================

/**
 * Exercise: Write tests for your components
 *
 * Use the ComponentTestUtils pattern from examples.js
 * to create tests for components you've built.
 */

async function runComponentTests() {
  const results = {
    passed: 0,
    failed: 0,
    tests: [],
  };

  async function test(name, fn) {
    try {
      await fn();
      results.passed++;
      results.tests.push({ name, status: 'passed' });
      console.log(`✓ ${name}`);
    } catch (error) {
      results.failed++;
      results.tests.push({ name, status: 'failed', error: error.message });
      console.error(`✗ ${name}: ${error.message}`);
    }
  }

  function assert(condition, message = 'Assertion failed') {
    if (!condition) throw new Error(message);
  }

  // TODO: Add your tests here

  // Example test structure:
  /*
    await test('NotificationManager shows notification', async () => {
        const manager = document.createElement('notification-manager');
        document.body.appendChild(manager);
        
        manager.show('Test message', 'success');
        
        const notification = manager.shadowRoot.querySelector('.notification');
        assert(notification, 'Notification should be rendered');
        assert(notification.textContent.includes('Test message'), 'Should show message');
        
        document.body.removeChild(manager);
    });
    
    await test('TodoApp adds todo', async () => {
        const app = document.createElement('todo-app');
        document.body.appendChild(app);
        
        app.addTodo('Test todo');
        
        const todoList = app.shadowRoot.querySelector('.todos');
        assert(todoList.children.length === 1, 'Should have one todo');
        
        document.body.removeChild(app);
    });
    */

  console.log(
    `\n${results.passed}/${results.passed + results.failed} tests passed`
  );
  return results;
}

// ============================================
// EXERCISE 8: Component Factory
// ============================================

/**
 * Exercise: Create a component factory function
 *
 * Requirements:
 * 1. Accepts a configuration object
 * 2. Returns a custom element class
 * 3. Supports common options: props, state, template, methods
 * 4. Includes lifecycle hooks
 * 5. Auto-registers the component
 */

function defineComponent(config) {
  // TODO: Implement component factory
  /*
    config = {
        name: 'my-component',
        
        // Observable attributes
        props: {
            title: { type: String, default: '' },
            count: { type: Number, default: 0 }
        },
        
        // Initial state
        state: {
            loading: false,
            data: null
        },
        
        // Template function
        template: (props, state) => `
            <div>${props.title}</div>
        `,
        
        // Styles
        styles: `
            :host { display: block; }
        `,
        
        // Lifecycle hooks
        onMount() {},
        onUnmount() {},
        onUpdate(changes) {},
        
        // Methods
        methods: {
            increment() {
                this.setState({ count: this.state.count + 1 });
            }
        }
    }
    */

  const ComponentClass = class extends HTMLElement {
    // TODO: Implement based on config
  };

  // TODO: Set up observedAttributes from props
  // TODO: Register custom element

  return ComponentClass;
}

// ============================================
// BONUS CHALLENGE: Full Featured Component
// ============================================

/**
 * Challenge: Build a complete Dropdown component
 *
 * Features:
 * 1. Searchable options
 * 2. Multi-select support
 * 3. Keyboard navigation
 * 4. Accessible (ARIA compliant)
 * 5. Form-associated
 * 6. Lazy loads options
 * 7. Customizable option rendering via slots
 * 8. Grouped options support
 * 9. Virtual scrolling for large lists
 * 10. Theme support via CSS custom properties
 *
 * This combines multiple patterns from the module!
 */

class AdvancedDropdown extends HTMLElement {
  static formAssociated = true;

  static get observedAttributes() {
    return [
      'placeholder',
      'multiple',
      'searchable',
      'disabled',
      'required',
      'value',
    ];
  }

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this._internals = this.attachInternals?.();

    // TODO: Initialize state
    // TODO: Set up keyboard navigation
    // TODO: Set up form integration
  }

  // TODO: Implement all features

  render() {
    this.shadowRoot.innerHTML = `
            <style>
                /* TODO: Add comprehensive styles */
            </style>
            <div class="dropdown">
                <!-- TODO: Implement dropdown structure -->
            </div>
        `;
  }
}

// TODO: Define custom element
// customElements.define('advanced-dropdown', AdvancedDropdown);

// ============================================
// SOLUTIONS VERIFICATION
// ============================================

function verifySolutions() {
  console.log('Verifying Component Pattern exercise solutions...\n');

  const exercises = [
    'NotificationManager',
    'TodoApp with Observable State',
    'DataGrid Composition',
    'TextEditor with Plugins',
    'VirtualList',
    'ThemeProvider/Consumer',
    'Component Tests',
    'Component Factory',
    'Advanced Dropdown (Bonus)',
  ];

  console.log('Exercises to complete:');
  exercises.forEach((ex, i) => console.log(`${i + 1}. ${ex}`));

  console.log('\n--- Testing Implementations ---\n');

  const definedElements = [
    'notification-manager',
    'todo-app',
    'data-grid',
    'grid-column',
    'text-editor',
    'virtual-list',
    'theme-provider',
    'theme-consumer',
    'advanced-dropdown',
  ];

  definedElements.forEach((name) => {
    const isDefined = customElements.get(name) !== undefined;
    const status = isDefined ? '✓' : '○';
    console.log(`${status} <${name}>`);
  });

  console.log('\nSolution guidelines:');
  console.log('1. All components should use Shadow DOM');
  console.log(
    '2. Components should clean up resources in disconnectedCallback'
  );
  console.log('3. State changes should trigger efficient re-renders');
  console.log('4. Events should bubble and be composed');
  console.log('5. Components should be accessible');
}

// Export for testing
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    NotificationManager,
    TodoApp,
    DataGrid,
    TextEditor,
    VirtualList,
    ThemeProvider,
    ThemeConsumer,
    defineComponent,
    AdvancedDropdown,
    runComponentTests,
    verifySolutions,
  };
}
Exercises - JavaScript Tutorial | DeepML