javascript

exercises

exercises.js
/**
 * Shadow DOM - Exercises
 *
 * Practice using Shadow DOM for encapsulation and composition
 */

// ============================================
// EXERCISE 1: Encapsulated Badge Component
// ============================================

/**
 * Create a badge component that:
 * - Uses Shadow DOM for style encapsulation
 * - Supports variants: primary, success, warning, error
 * - Has a pill shape option
 * - Uses slots for content
 * - External styles should NOT affect internal styling
 */

class BadgeComponent extends HTMLElement {
  constructor() {
    super();
    // TODO: Attach shadow root
  }

  static get observedAttributes() {
    // TODO: Return ['variant', 'pill']
  }

  connectedCallback() {
    // TODO: Render the badge
  }

  attributeChangedCallback(name, oldVal, newVal) {
    // TODO: Re-render on attribute change
  }

  render() {
    // TODO: Create scoped styles for:
    // - :host display
    // - .badge base styles
    // - Variant colors (primary, success, warning, error)
    // - Pill shape modifier
    // - Slot for content
  }
}

// customElements.define('badge-component', BadgeComponent);

/**
 * Expected Usage:
 * <badge-component variant="success" pill>New</badge-component>
 */

// ============================================
// EXERCISE 2: Modal Dialog with Slots
// ============================================

/**
 * Create a modal dialog that:
 * - Uses named slots for header, body, and footer
 * - Has backdrop that closes modal on click
 * - Provides ::part for external styling
 * - Traps focus inside modal (accessibility)
 * - Uses CSS custom properties for theming
 */

class ModalComponent extends HTMLElement {
  constructor() {
    super();
    // TODO: Attach shadow root
  }

  static get observedAttributes() {
    // TODO: Return ['open']
  }

  get open() {
    // TODO: Return open state
  }

  set open(value) {
    // TODO: Toggle 'open' attribute
  }

  show() {
    // TODO: Open the modal
    // TODO: Trap focus
  }

  hide() {
    // TODO: Close the modal
    // TODO: Restore focus
  }

  connectedCallback() {
    // TODO: Render modal structure
    // TODO: Add event listeners
  }

  render() {
    // TODO: Create styles with:
    // - CSS custom properties (--modal-bg, --modal-shadow, etc.)
    // - :host([open]) for visibility
    // - Backdrop styling
    // - Modal container with parts
    // - Named slots: header, (default), footer
    // - ::slotted() styling for headers and buttons
  }
}

// customElements.define('modal-component', ModalComponent);

/**
 * Expected Usage:
 * <modal-component id="myModal">
 *     <h2 slot="header">Modal Title</h2>
 *     <p>Modal content here</p>
 *     <button slot="footer">Cancel</button>
 *     <button slot="footer">Confirm</button>
 * </modal-component>
 *
 * // External theming:
 * modal-component {
 *     --modal-bg: #1a1a2e;
 *     --modal-color: white;
 * }
 *
 * modal-component::part(container) {
 *     border-radius: 16px;
 * }
 */

// ============================================
// EXERCISE 3: Themeable Navigation
// ============================================

/**
 * Create a navigation component that:
 * - Uses slots for nav items
 * - Exposes CSS custom properties for all colors
 * - Has responsive behavior (hamburger menu on mobile)
 * - Styles slotted anchor elements
 * - Highlights current page via 'active' attribute on items
 */

class NavComponent extends HTMLElement {
  constructor() {
    super();
    // TODO: Attach shadow root
    // TODO: Track mobile menu state
  }

  connectedCallback() {
    // TODO: Render navigation
    // TODO: Setup resize observer for responsive
  }

  disconnectedCallback() {
    // TODO: Cleanup observers
  }

  toggleMobileMenu() {
    // TODO: Toggle mobile menu visibility
  }

  render() {
    // TODO: Create styles with:
    // - CSS custom properties for all colors
    // - :host styles for container
    // - ::slotted(a) for link styling
    // - ::slotted(a[active]) for active state
    // - Mobile menu styles with @media query
    // - Hamburger button (hidden on desktop)
  }
}

// customElements.define('nav-component', NavComponent);

/**
 * Expected Usage:
 * <nav-component>
 *     <a href="/" active>Home</a>
 *     <a href="/about">About</a>
 *     <a href="/contact">Contact</a>
 * </nav-component>
 *
 * // Theming:
 * nav-component {
 *     --nav-bg: #1e3a5f;
 *     --nav-color: white;
 *     --nav-hover-bg: rgba(255,255,255,0.1);
 *     --nav-active-color: #fbbf24;
 * }
 */

// ============================================
// EXERCISE 4: Slot Observer Pattern
// ============================================

/**
 * Create a tabs component that:
 * - Uses slotchange to detect tab additions/removals
 * - Automatically generates tab buttons from slotted panels
 * - Shows only active panel content
 * - Syncs tab selection with panel visibility
 */

class TabsComponent extends HTMLElement {
  constructor() {
    super();
    // TODO: Attach shadow root
    // TODO: Track active tab index
  }

  get activeIndex() {
    // TODO: Return active tab index
  }

  set activeIndex(value) {
    // TODO: Set active tab and update UI
  }

  connectedCallback() {
    // TODO: Render tabs structure
    // TODO: Setup slotchange listener
  }

  _handleSlotChange(event) {
    // TODO: Get assigned elements
    // TODO: Generate tab buttons from panel titles
    // TODO: Update visibility
  }

  _selectTab(index) {
    // TODO: Update active index
    // TODO: Update tab buttons state
    // TODO: Show/hide panels
    // TODO: Dispatch 'tab-change' event
  }

  render() {
    // TODO: Create structure with:
    // - Tab button container
    // - Content slot for panels
    // - Styling for active/inactive states
  }
}

// customElements.define('tabs-component', TabsComponent);

/**
 * Expected Usage:
 * <tabs-component>
 *     <div data-tab="Overview">Overview content...</div>
 *     <div data-tab="Details">Details content...</div>
 *     <div data-tab="Settings">Settings content...</div>
 * </tabs-component>
 */

// ============================================
// EXERCISE 5: Nested Shadow DOM
// ============================================

/**
 * Create a nested component structure where:
 * - Parent component contains child components
 * - Each has its own Shadow DOM
 * - Events bubble correctly with composed: true
 * - Theming cascades via CSS custom properties
 */

class ListContainer extends HTMLElement {
  constructor() {
    super();
    // TODO: Attach shadow root
  }

  connectedCallback() {
    // TODO: Render container with slot for items
    // TODO: Listen for events from child items
  }

  render() {
    // TODO: Create container styles
    // TODO: Define CSS custom properties that children inherit
  }
}

class ListItem extends HTMLElement {
  constructor() {
    super();
    // TODO: Attach shadow root
  }

  connectedCallback() {
    // TODO: Render item
    // TODO: Dispatch events that cross shadow boundary
  }

  render() {
    // TODO: Create item styles using inherited custom properties
    // TODO: Add interactive elements
  }
}

// customElements.define('list-container', ListContainer);
// customElements.define('list-item', ListItem);

/**
 * Expected Usage:
 * <list-container>
 *     <list-item>Item 1</list-item>
 *     <list-item>Item 2</list-item>
 *     <list-item>Item 3</list-item>
 * </list-container>
 *
 * // Container can listen for item events
 * container.addEventListener('item-click', (e) => {
 *     console.log('Item clicked:', e.detail);
 * });
 */

// ============================================
// EXERCISE 6: Adopted StyleSheets
// ============================================

/**
 * Create components that share stylesheets using adoptedStyleSheets:
 * - Create a shared stylesheet for common styles
 * - Create component-specific stylesheets
 * - Combine them in the shadow root
 * - Demonstrate runtime style updates
 */

class SharedStylesBase {
  // TODO: Create static shared stylesheet
  static sharedStyles = null;

  static {
    // TODO: Initialize shared CSSStyleSheet if supported
    // Include common styles: colors, typography, spacing
  }

  static getSharedStyles() {
    // TODO: Return the shared stylesheet or null
  }
}

class StyledCard extends HTMLElement {
  static componentStyles = null;

  static {
    // TODO: Create component-specific stylesheet
  }

  constructor() {
    super();
    // TODO: Attach shadow root
    // TODO: Combine shared and component styles
  }

  connectedCallback() {
    // TODO: Render card content
  }

  updateStyles(cssText) {
    // TODO: Dynamically update component styles
  }
}

class StyledButton extends HTMLElement {
  static componentStyles = null;

  static {
    // TODO: Create component-specific stylesheet
  }

  constructor() {
    super();
    // TODO: Attach shadow root
    // TODO: Combine shared and component styles
  }

  connectedCallback() {
    // TODO: Render button
  }
}

// customElements.define('styled-card', StyledCard);
// customElements.define('styled-button', StyledButton);

// ============================================
// BONUS: Shadow DOM Polyfill Detection
// ============================================

/**
 * Create a component that:
 * - Detects if Shadow DOM is natively supported
 * - Falls back to a non-shadow approach if not
 * - Provides consistent API regardless of mode
 */

class PolyfillAware extends HTMLElement {
  constructor() {
    super();
    // TODO: Detect native Shadow DOM support
    // TODO: Conditionally attach shadow root
  }

  get root() {
    // TODO: Return shadow root or element itself
  }

  connectedCallback() {
    // TODO: Render to appropriate root
    // TODO: Handle scoped styles appropriately
  }

  render() {
    // TODO: Create styles that work in both modes
    // TODO: Use BEM or similar for fallback encapsulation
  }
}

// customElements.define('polyfill-aware', PolyfillAware);

// ============================================
// TEST HELPERS
// ============================================

function testShadowDOM() {
  console.log('=== Shadow DOM Exercise Tests ===\n');

  // Test badge
  console.log('1. BadgeComponent');
  console.log('   - Should encapsulate styles');
  console.log('   - Should support variants');
  console.log('   - External CSS should not leak in');

  // Test modal
  console.log('\n2. ModalComponent');
  console.log('   - Should use named slots correctly');
  console.log('   - Should be themeable via custom properties');
  console.log('   - Should expose ::part for styling');

  // Test navigation
  console.log('\n3. NavComponent');
  console.log('   - Should style slotted anchors');
  console.log('   - Should be responsive');
  console.log('   - Should be fully themeable');

  // Test tabs
  console.log('\n4. TabsComponent');
  console.log('   - Should detect slot changes');
  console.log('   - Should auto-generate tabs');
  console.log('   - Should manage panel visibility');

  // Test nested
  console.log('\n5. Nested Components');
  console.log('   - Events should bubble correctly');
  console.log('   - Theme should cascade');
  console.log('   - Each should have its own shadow');

  // Test adopted styles
  console.log('\n6. AdoptedStyleSheets');
  console.log('   - Should share common styles');
  console.log('   - Should support dynamic updates');
  console.log('   - Should fall back gracefully');
}

// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    BadgeComponent,
    ModalComponent,
    NavComponent,
    TabsComponent,
    ListContainer,
    ListItem,
    SharedStylesBase,
    StyledCard,
    StyledButton,
    PolyfillAware,
    testShadowDOM,
  };
}
Exercises - JavaScript Tutorial | DeepML