javascript
exercises
exercises.js⚡javascript
/**
* 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,
};
}