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