javascript
exercises
exercises.js⚡javascript
/**
* Web Components - Exercises
*
* Practice building reusable custom elements
*/
// ============================================
// EXERCISE 1: Tooltip Component
// ============================================
/**
* Create a tooltip custom element that:
* - Shows text when hovering over slotted content
* - Supports 'position' attribute (top, bottom, left, right)
* - Has smooth fade-in/fade-out animation
* - Uses Shadow DOM for encapsulation
*/
class TooltipElement extends HTMLElement {
static get observedAttributes() {
// TODO: Return observed attributes
}
constructor() {
super();
// TODO: Attach shadow root
// TODO: Initialize properties
}
connectedCallback() {
// TODO: Render the component
// TODO: Add hover event listeners
}
disconnectedCallback() {
// TODO: Cleanup event listeners
}
attributeChangedCallback(name, oldValue, newValue) {
// TODO: Handle attribute changes
}
show() {
// TODO: Show the tooltip
}
hide() {
// TODO: Hide the tooltip
}
render() {
// TODO: Render tooltip with:
// - Styles for positioning (top, bottom, left, right)
// - Arrow pointing to content
// - Fade animation
// - Slot for trigger content
// - Tooltip text from 'text' attribute
}
}
// customElements.define('tooltip-element', TooltipElement);
/**
* Expected Usage:
* <tooltip-element text="This is a tooltip" position="top">
* <button>Hover me</button>
* </tooltip-element>
*/
// ============================================
// EXERCISE 2: Accordion Component
// ============================================
/**
* Create an accordion custom element that:
* - Supports multiple collapsible sections
* - Has 'single' mode (only one open) or 'multiple' mode
* - Animates expansion/collapse
* - Dispatches events when sections open/close
* - Uses slots for header and content
*/
class AccordionContainer extends HTMLElement {
constructor() {
super();
// TODO: Attach shadow root
// TODO: Track open sections
}
connectedCallback() {
// TODO: Render container
// TODO: Listen for item events
}
get mode() {
// TODO: Return 'single' or 'multiple' based on attribute
}
openSection(index) {
// TODO: Open section by index
// TODO: In 'single' mode, close other sections
}
closeSection(index) {
// TODO: Close section by index
}
toggleSection(index) {
// TODO: Toggle section open/close
}
}
class AccordionItem extends HTMLElement {
static get observedAttributes() {
// TODO: Return ['open'] for tracking state
}
constructor() {
super();
// TODO: Attach shadow root
}
get open() {
// TODO: Return open state
}
set open(value) {
// TODO: Set open state
// TODO: Dispatch 'toggle' event
}
connectedCallback() {
// TODO: Render item
// TODO: Add header click listener
}
attributeChangedCallback(name, oldVal, newVal) {
// TODO: Handle 'open' attribute changes
}
render() {
// TODO: Render with:
// - Header slot with expand/collapse icon
// - Content slot (hidden when collapsed)
// - Smooth height animation
}
}
// customElements.define('accordion-container', AccordionContainer);
// customElements.define('accordion-item', AccordionItem);
/**
* Expected Usage:
* <accordion-container mode="single">
* <accordion-item>
* <span slot="header">Section 1</span>
* <div slot="content">Content 1</div>
* </accordion-item>
* <accordion-item open>
* <span slot="header">Section 2</span>
* <div slot="content">Content 2</div>
* </accordion-item>
* </accordion-container>
*/
// ============================================
// EXERCISE 3: Data Table Component
// ============================================
/**
* Create a data table custom element that:
* - Accepts data via property or JSON attribute
* - Supports column definitions with labels and formatters
* - Has sortable columns
* - Supports row selection (single or multiple)
* - Dispatches events for sort and selection changes
*/
class DataTable extends HTMLElement {
static get observedAttributes() {
// TODO: Return observed attributes
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
// TODO: Initialize:
// - _data array
// - _columns array
// - _sortColumn and _sortDirection
// - _selectedRows Set
}
// Data property
get data() {
// TODO: Return data array
}
set data(value) {
// TODO: Set data and re-render
}
// Columns property
get columns() {
// TODO: Return columns array
}
set columns(value) {
// TODO: Set columns and re-render
}
connectedCallback() {
// TODO: Parse data from attribute if present
// TODO: Initial render
}
sortBy(column) {
// TODO: Sort data by column
// TODO: Toggle direction if same column
// TODO: Dispatch 'sort' event
}
selectRow(index, multi = false) {
// TODO: Select row (toggle if already selected)
// TODO: Clear others if not multi-select
// TODO: Dispatch 'selection-change' event
}
getSelectedRows() {
// TODO: Return array of selected row data
}
render() {
// TODO: Render table with:
// - <thead> with sortable column headers
// - <tbody> with data rows
// - Sort indicators (↑/↓)
// - Selected row styling
// - Responsive design
}
}
// customElements.define('data-table', DataTable);
/**
* Expected Usage:
* const table = document.querySelector('data-table');
* table.columns = [
* { key: 'name', label: 'Name', sortable: true },
* { key: 'age', label: 'Age', sortable: true },
* { key: 'email', label: 'Email' }
* ];
* table.data = [
* { name: 'John', age: 30, email: 'john@example.com' },
* { name: 'Jane', age: 25, email: 'jane@example.com' }
* ];
*/
// ============================================
// EXERCISE 4: Form Validation Component
// ============================================
/**
* Create a form-associated custom input that:
* - Integrates with native forms
* - Supports validation rules (required, minlength, pattern, etc.)
* - Shows inline error messages
* - Has visual feedback for valid/invalid states
* - Supports custom validation functions
*/
class ValidatedInput extends HTMLElement {
static formAssociated = true;
static get observedAttributes() {
// TODO: Return validation-related attributes
}
constructor() {
super();
// TODO: Attach internals and shadow root
// TODO: Initialize validation state
}
get value() {
// TODO: Return input value
}
set value(val) {
// TODO: Set value and validate
}
get validity() {
// TODO: Return validity state object
}
connectedCallback() {
// TODO: Render input with error message area
// TODO: Setup input event listeners
}
validate() {
// TODO: Run all validation rules
// TODO: Update visual state
// TODO: Set form validity via internals
// TODO: Return validation result
}
setCustomValidity(message) {
// TODO: Set custom error message
}
// Form callbacks
formResetCallback() {
// TODO: Reset to default value
}
formStateRestoreCallback(state) {
// TODO: Restore value from state
}
render() {
// TODO: Render with:
// - Styled input element
// - Error message display
// - Valid/invalid visual states
// - Required indicator
}
}
// customElements.define('validated-input', ValidatedInput);
/**
* Expected Usage:
* <form>
* <validated-input
* name="email"
* type="email"
* required
* pattern="[a-z]+@[a-z]+\.[a-z]+"
* error-message="Please enter a valid email"
* ></validated-input>
* <button type="submit">Submit</button>
* </form>
*/
// ============================================
// EXERCISE 5: Notification Toast Component
// ============================================
/**
* Create a notification toast system with:
* - A container component that manages toasts
* - Individual toast components
* - Auto-dismiss with configurable duration
* - Types: success, error, warning, info
* - Position options (top-right, bottom-right, etc.)
* - Stack management
*/
class ToastContainer extends HTMLElement {
constructor() {
super();
// TODO: Attach shadow root
// TODO: Initialize toast queue
}
static get instance() {
// TODO: Singleton pattern - return or create instance
}
connectedCallback() {
// TODO: Render container
// TODO: Set position from attribute
}
show(message, options = {}) {
// TODO: Create and add new toast
// TODO: Return toast element for chaining
}
success(message, options = {}) {
// TODO: Show success toast
}
error(message, options = {}) {
// TODO: Show error toast
}
warning(message, options = {}) {
// TODO: Show warning toast
}
info(message, options = {}) {
// TODO: Show info toast
}
dismissAll() {
// TODO: Remove all toasts
}
}
class ToastNotification extends HTMLElement {
static get observedAttributes() {
// TODO: Return ['type', 'duration', 'dismissible']
}
constructor() {
super();
// TODO: Attach shadow root
// TODO: Initialize timer
}
connectedCallback() {
// TODO: Render toast
// TODO: Start auto-dismiss timer
// TODO: Add entrance animation
}
disconnectedCallback() {
// TODO: Clear timer
}
dismiss() {
// TODO: Play exit animation
// TODO: Remove from DOM
// TODO: Dispatch 'dismiss' event
}
render() {
// TODO: Render with:
// - Type-specific styling and icons
// - Message slot or text
// - Dismiss button (if dismissible)
// - Progress bar showing time remaining
}
}
// customElements.define('toast-container', ToastContainer);
// customElements.define('toast-notification', ToastNotification);
/**
* Expected Usage:
* // Get or create singleton container
* const toast = ToastContainer.instance;
*
* // Show different types
* toast.success('Data saved successfully!');
* toast.error('Failed to load data', { duration: 5000 });
* toast.warning('Your session will expire soon');
* toast.info('New version available');
*/
// ============================================
// EXERCISE 6: Draggable List Component
// ============================================
/**
* Create a draggable list component that:
* - Allows reordering items via drag and drop
* - Provides visual feedback during drag
* - Dispatches events when order changes
* - Works with slotted content
* - Supports keyboard navigation
*/
class DraggableList extends HTMLElement {
constructor() {
super();
// TODO: Attach shadow root
// TODO: Track dragging state
}
connectedCallback() {
// TODO: Render list container
// TODO: Setup drag event listeners
// TODO: Setup keyboard event listeners
}
disconnectedCallback() {
// TODO: Cleanup event listeners
}
getOrder() {
// TODO: Return current order of items
}
setOrder(order) {
// TODO: Reorder items based on array
}
_handleDragStart(e, item) {
// TODO: Start drag operation
// TODO: Set drag data
// TODO: Add dragging class
}
_handleDragOver(e) {
// TODO: Determine drop position
// TODO: Show drop indicator
}
_handleDrop(e, targetItem) {
// TODO: Complete reorder
// TODO: Dispatch 'reorder' event
}
_handleKeyDown(e, item) {
// TODO: Handle arrow keys for keyboard reordering
}
render() {
// TODO: Render with:
// - Slot for list items
// - Drag handle styling
// - Drop indicator
// - Accessible ARIA attributes
}
}
// customElements.define('draggable-list', DraggableList);
/**
* Expected Usage:
* <draggable-list>
* <div class="item" data-id="1">Item 1</div>
* <div class="item" data-id="2">Item 2</div>
* <div class="item" data-id="3">Item 3</div>
* </draggable-list>
*
* list.addEventListener('reorder', (e) => {
* console.log('New order:', e.detail.order);
* });
*/
// ============================================
// BONUS CHALLENGE: Component Library Framework
// ============================================
/**
* Build a mini component framework that:
* - Provides a base class with common functionality
* - Includes reactive state management
* - Supports computed properties
* - Has event delegation helpers
* - Provides template rendering utilities
*/
class ComponentFramework {
// TODO: Implement createComponent factory
static createComponent(config) {
// config = {
// name: 'my-component',
// template: '<div>...</div>',
// styles: '...',
// props: { count: { type: Number, default: 0 } },
// state: { ... },
// computed: { ... },
// methods: { ... },
// lifecycle: { mounted, unmounted, ... }
// }
}
// TODO: Implement reactive state system
static createReactiveState(initialState, onChange) {
// Return Proxy that triggers onChange on mutations
}
// TODO: Implement template engine
static parseTemplate(template, data) {
// Handle {{ expressions }}
// Handle @event bindings
// Handle :attribute bindings
}
}
/**
* Expected Usage:
* ComponentFramework.createComponent({
* name: 'counter-component',
* template: `
* <button @click="decrement">-</button>
* <span>{{ count }}</span>
* <button @click="increment">+</button>
* `,
* styles: `
* button { padding: 10px; }
* `,
* props: {
* initial: { type: Number, default: 0 }
* },
* state() {
* return { count: this.initial };
* },
* methods: {
* increment() { this.state.count++; },
* decrement() { this.state.count--; }
* }
* });
*/
// ============================================
// TEST HELPERS
// ============================================
function testWebComponents() {
console.log('=== Web Components Exercise Tests ===\n');
// Test tooltip
console.log('1. TooltipElement');
console.log(' - Should show on hover');
console.log(' - Should position correctly');
console.log(' - Should animate smoothly');
// Test accordion
console.log('\n2. Accordion');
console.log(' - Should expand/collapse sections');
console.log(' - Single mode should close others');
console.log(' - Should dispatch toggle events');
// Test data table
console.log('\n3. DataTable');
console.log(' - Should render columns and data');
console.log(' - Should sort on header click');
console.log(' - Should handle row selection');
// Test validated input
console.log('\n4. ValidatedInput');
console.log(' - Should validate on input');
console.log(' - Should integrate with form');
console.log(' - Should show error messages');
// Test toast
console.log('\n5. Toast System');
console.log(' - Should show notifications');
console.log(' - Should auto-dismiss');
console.log(' - Should stack properly');
// Test draggable list
console.log('\n6. DraggableList');
console.log(' - Should reorder on drag');
console.log(' - Should support keyboard');
console.log(' - Should dispatch events');
}
// Export for module usage
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
TooltipElement,
AccordionContainer,
AccordionItem,
DataTable,
ValidatedInput,
ToastContainer,
ToastNotification,
DraggableList,
ComponentFramework,
testWebComponents,
};
}