Model-View-Controller (MVC)
MVC is an architectural pattern that separates an application into three interconnected components: the Model (data and business logic), the View (presentation layer), and the Controller (orchestration and user input handling).
Key Concepts
- Model - Manages data and business logic, notifies observers of changes
- View - Renders the UI and binds user interactions to controller handlers
- Controller - Responds to user input, manipulates the model, coordinates updates
- Separation of Concerns - Each component has a single, well-defined responsibility
Interactive Demo
This task manager demonstrates MVC with clear separation. Try adding, completing, and deleting tasks while watching the event log.
Event Log
Watch how the Controller coordinates interactions between Model and View:
[System] MVC demo initialized
Source Code
Model
// MODEL - Data and Business Logic
class TaskModel {
constructor() {
this.tasks = [];
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
notifyObservers() {
this.observers.forEach(obs => obs.update(this.tasks));
}
addTask(title) {
const task = {
id: Date.now(),
title,
completed: false,
createdAt: new Date().toISOString()
};
this.tasks.push(task);
this.notifyObservers();
return task;
}
toggleTask(id) {
const task = this.tasks.find(t => t.id === id);
if (task) {
task.completed = !task.completed;
this.notifyObservers();
}
}
deleteTask(id) {
this.tasks = this.tasks.filter(t => t.id !== id);
this.notifyObservers();
}
getTasks() {
return this.tasks;
}
} View
// VIEW - Presentation Layer
class TaskView {
constructor() {
this.taskList = document.getElementById('mvc-task-list');
this.taskInput = document.getElementById('mvc-task-input');
this.addButton = document.getElementById('mvc-add-button');
this.stats = document.getElementById('mvc-stats');
}
update(tasks) {
this.renderTasks(tasks);
this.renderStats(tasks);
}
renderTasks(tasks) {
if (!this.taskList) return;
this.taskList.innerHTML = tasks.map(task => `
<li class="task-item ${task.completed ? 'completed' : ''}">
<input type="checkbox"
data-id="${task.id}"
${task.completed ? 'checked' : ''}>
<span>${task.title}</span>
<button class="delete-btn" data-id="${task.id}">Delete</button>
</li>
`).join('');
}
renderStats(tasks) {
if (!this.stats) return;
const total = tasks.length;
const completed = tasks.filter(t => t.completed).length;
const active = total - completed;
this.stats.innerHTML = `
<div>Total: ${total}</div>
<div>Active: ${active}</div>
<div>Completed: ${completed}</div>
`;
}
getTaskInput() {
return this.taskInput.value;
}
clearInput() {
this.taskInput.value = '';
}
bindAddTask(handler) {
this.addButton.addEventListener('click', () => {
const title = this.getTaskInput();
if (title.trim()) {
handler(title);
this.clearInput();
}
});
}
bindToggleTask(handler) {
this.taskList.addEventListener('change', (e) => {
if (e.target.type === 'checkbox') {
handler(parseInt(e.target.dataset.id));
}
});
}
bindDeleteTask(handler) {
this.taskList.addEventListener('click', (e) => {
if (e.target.classList.contains('delete-btn')) {
handler(parseInt(e.target.dataset.id));
}
});
}
} Controller
// CONTROLLER - Orchestration Layer
class TaskController {
constructor(model, view) {
this.model = model;
this.view = view;
// Model observes itself and updates view
this.model.addObserver(this.view);
// View binds to controller handlers
this.view.bindAddTask(this.handleAddTask.bind(this));
this.view.bindToggleTask(this.handleToggleTask.bind(this));
this.view.bindDeleteTask(this.handleDeleteTask.bind(this));
// Initial render
this.view.update(this.model.getTasks());
}
handleAddTask(title) {
this.log(`Controller: Adding task "${title}"`);
this.model.addTask(title);
}
handleToggleTask(id) {
this.log(`Controller: Toggling task #${id}`);
this.model.toggleTask(id);
}
handleDeleteTask(id) {
this.log(`Controller: Deleting task #${id}`);
this.model.deleteTask(id);
}
log(message) {
const logEl = document.getElementById('mvc-log');
if (logEl) {
const entry = document.createElement('div');
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logEl.appendChild(entry);
logEl.scrollTop = logEl.scrollHeight;
}
}
} Initialization
// INITIALIZATION
const model = new TaskModel();
const view = new TaskView();
const controller = new TaskController(model, view);
// Add some sample tasks
model.addTask('Learn MVC Pattern');
model.addTask('Build a web application');
model.addTask('Master separation of concerns'); Benefits of MVC
- Separation of Concerns - Each layer has a clear responsibility
- Testability - Model and Controller can be tested independently of the UI
- Maintainability - Changes to one layer don't cascade to others
- Reusability - Models can serve multiple views, views can work with different models
- Parallel Development - Different team members can work on different layers
Trade-offs
- Complexity - More files and abstractions than simple procedural code
- Indirection - Following the flow of execution requires understanding all three layers
- Boilerplate - Observer patterns and event binding add code overhead
- Overkill - Simple UIs may not benefit from this level of separation