COMP 305: Object-Oriented Software Design

University of San Diego, Fall 2025

Component-Based Architecture

Component-Based Architecture organizes applications as collections of self-contained, reusable components. Each component encapsulates its structure (HTML), presentation (CSS), and behavior (JavaScript), providing a clear contract for interaction with other components.

Core Concepts

1. Encapsulation

Components hide their internal implementation details and expose only a public interface. This prevents external code from depending on internal structure.

// Web Component with encapsulation
class UserCard extends HTMLElement {
    // Private data (not accessible from outside)
    #userData = null;

    constructor() {
        super();
        // Shadow DOM provides true encapsulation
        this.attachShadow({ mode: 'open' });
    }

    // Public interface
    set user(data) {
        this.#userData = data;
        this.#render();
    }

    // Private method
    #render() {
        this.shadowRoot.innerHTML = `
            <style>
                /* These styles won't leak out */
                .card {
                    border: 1px solid #ccc;
                    padding: 1rem;
                    border-radius: 8px;
                }
                .name {
                    font-weight: bold;
                    font-size: 1.2rem;
                }
            </style>
            <div class="card">
                <div class="name">${this.#userData?.name || 'Unknown'}</div>
                <div class="email">${this.#userData?.email || 'No email'}</div>
            </div>
        `;
    }
}

customElements.define('user-card', UserCard);

Usage in HTML:

<!-- Clean, declarative component use -->
<user-card></user-card>

<script is:inline>
    const card = document.querySelector('user-card');
    // Only public interface is accessible
    card.user = {
        name: 'Alice Johnson',
        email: 'alice@example.com'
    };
</script>

<!-- Styles from the component don't affect the page -->
<style>
    .card { background: red; } /* Won't affect the component's .card */
</style>

2. Reusability

Components can be used multiple times throughout an application. They maintain their own state and behavior regardless of where they're placed.

// Reusable button component
class ActionButton extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
    }

    connectedCallback() {
        const variant = this.getAttribute('variant') || 'primary';
        const disabled = this.hasAttribute('disabled');

        this.shadowRoot.innerHTML = `
            <style>
                button {
                    padding: 0.5rem 1rem;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    font-size: 1rem;
                }
                button.primary { background: #007bff; color: white; }
                button.secondary { background: #6c757d; color: white; }
                button.danger { background: #dc3545; color: white; }
                button:disabled {
                    opacity: 0.5;
                    cursor: not-allowed;
                }
            </style>
            <button class="${variant}" ${disabled ? 'disabled' : ''}>
                <slot></slot>
            </button>
        `;

        // Forward click events
        this.shadowRoot.querySelector('button').addEventListener('click', () => {
            this.dispatchEvent(new Event('action', { bubbles: true }));
        });
    }
}

customElements.define('action-button', ActionButton);

Reuse across the application:

<!-- Same component, different contexts -->
<action-button variant="primary">Save</action-button>
<action-button variant="secondary">Cancel</action-button>
<action-button variant="danger">Delete</action-button>
<action-button disabled>Processing...</action-button>

3. Composition

Complex UIs are built by composing smaller components together. Components can contain other components, creating a component hierarchy.

// Container component that composes other components
class UserProfile extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
    }

    set user(data) {
        this._user = data;
        this.render();
    }

    render() {
        this.shadowRoot.innerHTML = `
            <style>
                .profile {
                    display: flex;
                    flex-direction: column;
                    gap: 1rem;
                    padding: 1rem;
                }
            </style>
            <div class="profile">
                <!-- Composed of smaller components -->
                <user-avatar src="${this._user.avatar}"></user-avatar>
                <user-card></user-card>
                <user-stats></user-stats>
                <action-button variant="primary">Edit Profile</action-button>
            </div>
        `;

        // Pass data down to child components
        this.shadowRoot.querySelector('user-card').user = this._user;
        this.shadowRoot.querySelector('user-stats').stats = {
            posts: this._user.postCount,
            followers: this._user.followerCount
        };
    }
}

customElements.define('user-profile', UserProfile);

4. Lifecycle Management

Web Components have lifecycle callbacks that allow components to respond to creation, insertion, updates, and removal.

class DataTable extends HTMLElement {
    constructor() {
        super();
        // Called when element is created
        console.log('Component created');
        this.attachShadow({ mode: 'open' });
    }

    connectedCallback() {
        // Called when element is added to DOM
        console.log('Component mounted to DOM');
        this.fetchData();
    }

    disconnectedCallback() {
        // Called when element is removed from DOM
        console.log('Component removed from DOM');
        this.cleanup();
    }

    attributeChangedCallback(name, oldValue, newValue) {
        // Called when observed attributes change
        console.log(`Attribute ${name} changed from ${oldValue} to ${newValue}`);
        if (name === 'data-source') {
            this.fetchData();
        }
    }

    // Tell which attributes to observe
    static get observedAttributes() {
        return ['data-source'];
    }

    async fetchData() {
        const source = this.getAttribute('data-source');
        if (source) {
            const response = await fetch(source);
            this.data = await response.json();
            this.render();
        }
    }

    cleanup() {
        // Cancel any pending requests, clear timers, etc.
        if (this._timer) {
            clearInterval(this._timer);
        }
    }

    render() {
        // Update the DOM
    }
}

customElements.define('data-table', DataTable);

Component Communication

Props (Attributes and Properties)

Parent components pass data down to children via attributes or properties.

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

    // Property setter
    set value(val) {
        this.setAttribute('value', val);
    }

    get value() {
        return this.getAttribute('value') || '0';
    }

    static get observedAttributes() {
        return ['value', 'max'];
    }

    attributeChangedCallback() {
        this.render();
    }

    render() {
        const value = parseInt(this.value);
        const max = parseInt(this.getAttribute('max') || '100');
        const percent = (value / max) * 100;

        this.shadowRoot.innerHTML = `
            <style>
                .progress {
                    width: 100%;
                    height: 20px;
                    background: #e0e0e0;
                    border-radius: 10px;
                    overflow: hidden;
                }
                .bar {
                    height: 100%;
                    background: #4caf50;
                    transition: width 0.3s ease;
                }
            </style>
            <div class="progress">
                <div class="bar" style="width: ${percent}%"></div>
            </div>
        `;
    }

    connectedCallback() {
        this.render();
    }
}

customElements.define('progress-bar', ProgressBar);

Usage:

<!-- Via attributes -->
<progress-bar value="75" max="100"></progress-bar>

<!-- Via properties -->
<script is:inline>
    const bar = document.querySelector('progress-bar');
    bar.value = 50; // Updates the component
</script>

Slots for Content Projection

Slots allow parent components to pass content (including other components) into specific locations within a child component.

class DialogBox extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                .dialog {
                    border: 1px solid #ccc;
                    border-radius: 8px;
                    padding: 1rem;
                    background: white;
                    box-shadow: 0 2px 10px rgba(0,0,0,0.1);
                }
                .header {
                    font-size: 1.5rem;
                    font-weight: bold;
                    margin-bottom: 1rem;
                }
                .footer {
                    margin-top: 1rem;
                    display: flex;
                    justify-content: flex-end;
                    gap: 0.5rem;
                }
            </style>
            <div class="dialog">
                <div class="header">
                    <slot name="title">Default Title</slot>
                </div>
                <div class="content">
                    <slot>Default content</slot>
                </div>
                <div class="footer">
                    <slot name="actions"></slot>
                </div>
            </div>
        `;
    }
}

customElements.define('dialog-box', DialogBox);

Using slots:

<dialog-box>
    <!-- Named slot -->
    <span slot="title">Confirm Delete</span>

    <!-- Default slot -->
    <p>Are you sure you want to delete this item? This action cannot be undone.</p>

    <!-- Named slot -->
    <div slot="actions">
        <action-button variant="secondary">Cancel</action-button>
        <action-button variant="danger">Delete</action-button>
    </div>
</dialog-box>

Benefits

Challenges

Real-World Example: Todo List

// Todo Item Component
class TodoItem extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
    }

    set todo(data) {
        this._todo = data;
        this.render();
    }

    render() {
        const { id, text, completed } = this._todo;
        this.shadowRoot.innerHTML = `
            <style>
                .todo-item {
                    display: flex;
                    gap: 0.5rem;
                    padding: 0.5rem;
                    border-bottom: 1px solid #eee;
                }
                .completed { text-decoration: line-through; opacity: 0.6; }
            </style>
            <div class="todo-item">
                <input type="checkbox" ${completed ? 'checked' : ''}>
                <span class="${completed ? 'completed' : ''}">${text}</span>
                <button class="delete">Delete</button>
            </div>
        `;

        // Event handlers
        this.shadowRoot.querySelector('input').addEventListener('change', (e) => {
            this.dispatchEvent(new CustomEvent('toggle', {
                detail: { id },
                bubbles: true
            }));
        });

        this.shadowRoot.querySelector('.delete').addEventListener('click', () => {
            this.dispatchEvent(new CustomEvent('delete', {
                detail: { id },
                bubbles: true
            }));
        });
    }
}

// Todo List Component
class TodoList extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.todos = [];
    }

    connectedCallback() {
        this.render();
        this.setupEventListeners();
    }

    setupEventListeners() {
        // Listen for events from child components
        this.addEventListener('toggle', (e) => {
            this.toggleTodo(e.detail.id);
        });

        this.addEventListener('delete', (e) => {
            this.deleteTodo(e.detail.id);
        });
    }

    addTodo(text) {
        this.todos.push({
            id: Date.now(),
            text,
            completed: false
        });
        this.render();
    }

    toggleTodo(id) {
        const todo = this.todos.find(t => t.id === id);
        if (todo) {
            todo.completed = !todo.completed;
            this.render();
        }
    }

    deleteTodo(id) {
        this.todos = this.todos.filter(t => t.id !== id);
        this.render();
    }

    render() {
        this.shadowRoot.innerHTML = `
            <style>
                .container {
                    max-width: 500px;
                    margin: 0 auto;
                }
                .input-area {
                    display: flex;
                    gap: 0.5rem;
                    margin-bottom: 1rem;
                }
                input {
                    flex: 1;
                    padding: 0.5rem;
                }
            </style>
            <div class="container">
                <div class="input-area">
                    <input type="text" placeholder="Add todo..." />
                    <button>Add</button>
                </div>
                <div class="todo-items"></div>
            </div>
        `;

        // Render todo items
        const container = this.shadowRoot.querySelector('.todo-items');
        this.todos.forEach(todo => {
            const item = document.createElement('todo-item');
            item.todo = todo;
            container.appendChild(item);
        });

        // Add input handler
        const input = this.shadowRoot.querySelector('input');
        const button = this.shadowRoot.querySelector('button');

        button.addEventListener('click', () => {
            if (input.value.trim()) {
                this.addTodo(input.value);
                input.value = '';
            }
        });

        input.addEventListener('keypress', (e) => {
            if (e.key === 'Enter' && input.value.trim()) {
                this.addTodo(input.value);
                input.value = '';
            }
        });
    }
}

customElements.define('todo-item', TodoItem);
customElements.define('todo-list', TodoList);

Usage:

<!DOCTYPE html>
<html>
<head>
    <title>Todo App</title>
</head>
<body>
    <!-- Entire application in one component -->
    <todo-list></todo-list>

    <script src="todo-components.js"></script>
</body>
</html>

Key Takeaways

See Also