Explore practical applications of the Model-View-Controller (MVC) pattern in JavaScript and TypeScript, with examples in web, mobile, and desktop applications.
The Model-View-Controller (MVC) architectural pattern is a cornerstone in software development, particularly in web applications. It provides a structured approach to separating concerns, which enhances maintainability and testability. In this section, we will delve into practical applications of MVC in various domains, including web, mobile, and desktop software. We’ll also explore popular frameworks that leverage MVC, such as AngularJS, and compare MVC to other architectural patterns like MVVM. Finally, we’ll offer best practices and highlight common pitfalls to avoid.
Before diving into specific use cases, let’s briefly revisit the core components of MVC:
Let’s start with a classic example of a web application: a to-do list. This application will demonstrate how MVC can be applied to manage tasks effectively.
Model (JavaScript/TypeScript):
// Task.ts
export class Task {
constructor(public id: number, public title: string, public completed: boolean = false) {}
toggleCompletion(): void {
this.completed = !this.completed;
}
}
View (HTML and JavaScript/TypeScript):
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>To-Do List</title>
</head>
<body>
<div id="app">
<h1>To-Do List</h1>
<ul id="task-list"></ul>
<input type="text" id="new-task" placeholder="New task">
<button id="add-task">Add Task</button>
</div>
<script src="controller.js" type="module"></script>
</body>
</html>
Controller (JavaScript/TypeScript):
// controller.ts
import { Task } from './Task';
class TaskController {
private tasks: Task[] = [];
private taskListElement: HTMLElement | null = document.getElementById('task-list');
private newTaskInput: HTMLInputElement | null = document.getElementById('new-task') as HTMLInputElement;
private addTaskButton: HTMLElement | null = document.getElementById('add-task');
constructor() {
this.addTaskButton?.addEventListener('click', () => this.addTask());
this.render();
}
addTask(): void {
if (this.newTaskInput?.value) {
const task = new Task(this.tasks.length + 1, this.newTaskInput.value);
this.tasks.push(task);
this.newTaskInput.value = '';
this.render();
}
}
toggleTaskCompletion(taskId: number): void {
const task = this.tasks.find(t => t.id === taskId);
if (task) {
task.toggleCompletion();
this.render();
}
}
render(): void {
if (this.taskListElement) {
this.taskListElement.innerHTML = '';
this.tasks.forEach(task => {
const taskItem = document.createElement('li');
taskItem.textContent = task.title;
taskItem.style.textDecoration = task.completed ? 'line-through' : 'none';
taskItem.addEventListener('click', () => this.toggleTaskCompletion(task.id));
this.taskListElement?.appendChild(taskItem);
});
}
}
}
new TaskController();
Explanation:
Task
) manages the data and business logic, such as toggling the completion status of a task.TaskController
) handles user interactions, updates the model, and refreshes the view.While MVC is more commonly associated with web development, it can also be applied to mobile applications. Frameworks like React Native and Flutter allow developers to implement MVC-like architectures.
Model (JavaScript/TypeScript):
// Note.ts
export class Note {
constructor(public id: number, public content: string) {}
}
View (React Native Component):
// NoteView.tsx
import React from 'react';
import { View, Text, TextInput, Button, FlatList } from 'react-native';
import { Note } from './Note';
interface NoteViewProps {
notes: Note[];
onAddNote: (content: string) => void;
}
export const NoteView: React.FC<NoteViewProps> = ({ notes, onAddNote }) => {
const [newNoteContent, setNewNoteContent] = React.useState('');
return (
<View>
<Text>Notes</Text>
<FlatList
data={notes}
renderItem={({ item }) => <Text>{item.content}</Text>}
keyExtractor={item => item.id.toString()}
/>
<TextInput
value={newNoteContent}
onChangeText={setNewNoteContent}
placeholder="New note"
/>
<Button title="Add Note" onPress={() => {
onAddNote(newNoteContent);
setNewNoteContent('');
}} />
</View>
);
};
Controller (React Native Component):
// NoteController.tsx
import React from 'react';
import { Note } from './Note';
import { NoteView } from './NoteView';
export const NoteController: React.FC = () => {
const [notes, setNotes] = React.useState<Note[]>([]);
const addNote = (content: string) => {
const newNote = new Note(notes.length + 1, content);
setNotes([...notes, newNote]);
};
return (
<NoteView notes={notes} onAddNote={addNote} />
);
};
Explanation:
Note
) represents the data structure for a note.NoteView
) is a React Native component that displays notes and provides input for new notes.NoteController
) manages the state and logic for adding notes.MVC can also be applied to desktop applications, particularly those built with frameworks like Electron, which allows web technologies to be used for desktop development.
Model (JavaScript/TypeScript):
// CalculatorModel.ts
export class CalculatorModel {
private currentValue: number = 0;
add(value: number): number {
this.currentValue += value;
return this.currentValue;
}
subtract(value: number): number {
this.currentValue -= value;
return this.currentValue;
}
getCurrentValue(): number {
return this.currentValue;
}
}
View (HTML and JavaScript/TypeScript):
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Calculator</title>
</head>
<body>
<div id="calculator">
<input type="text" id="display" readonly>
<button id="add">Add</button>
<button id="subtract">Subtract</button>
</div>
<script src="controller.js" type="module"></script>
</body>
</html>
Controller (JavaScript/TypeScript):
// controller.ts
import { CalculatorModel } from './CalculatorModel';
class CalculatorController {
private model: CalculatorModel = new CalculatorModel();
private display: HTMLInputElement | null = document.getElementById('display') as HTMLInputElement;
private addButton: HTMLElement | null = document.getElementById('add');
private subtractButton: HTMLElement | null = document.getElementById('subtract');
constructor() {
this.addButton?.addEventListener('click', () => this.updateDisplay(this.model.add(1)));
this.subtractButton?.addEventListener('click', () => this.updateDisplay(this.model.subtract(1)));
this.updateDisplay(this.model.getCurrentValue());
}
updateDisplay(value: number): void {
if (this.display) {
this.display.value = value.toString();
}
}
}
new CalculatorController();
Explanation:
CalculatorModel
) handles the arithmetic operations and maintains the current value.CalculatorController
) links the model and view, updating the display based on user interactions.Many popular frameworks implement MVC or MVC-like patterns to help developers structure their applications efficiently.
AngularJS is a widely-used framework that follows the MVC pattern. It allows developers to create dynamic web applications by separating the application logic from the user interface.
Example: AngularJS MVC Structure
// app.js
angular.module('todoApp', [])
.controller('TodoController', function($scope) {
$scope.tasks = [];
$scope.addTask = function(taskTitle) {
$scope.tasks.push({ title: taskTitle, completed: false });
$scope.newTaskTitle = '';
};
$scope.toggleCompletion = function(task) {
task.completed = !task.completed;
};
});
<!-- index.html -->
<!DOCTYPE html>
<html lang="en" ng-app="todoApp">
<head>
<meta charset="UTF-8">
<title>To-Do List</title>
</head>
<body ng-controller="TodoController">
<h1>To-Do List</h1>
<ul>
<li ng-repeat="task in tasks" ng-click="toggleCompletion(task)" ng-style="{ 'text-decoration': task.completed ? 'line-through' : 'none' }">
{{ task.title }}
</li>
</ul>
<input type="text" ng-model="newTaskTitle" placeholder="New task">
<button ng-click="addTask(newTaskTitle)">Add Task</button>
</body>
</html>
Explanation:
The Model-View-ViewModel (MVVM) pattern is another architectural pattern that is often compared to MVC. MVVM is commonly used in frameworks like Angular and React.
// app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<h1>To-Do List</h1>
<ul>
<li *ngFor="let task of tasks" (click)="toggleCompletion(task)" [style.textDecoration]="task.completed ? 'line-through' : 'none'">
{{ task.title }}
</li>
</ul>
<input [(ngModel)]="newTaskTitle" placeholder="New task">
<button (click)="addTask()">Add Task</button>
`
})
export class AppComponent {
tasks = [];
newTaskTitle = '';
addTask() {
if (this.newTaskTitle) {
this.tasks.push({ title: this.newTaskTitle, completed: false });
this.newTaskTitle = '';
}
}
toggleCompletion(task) {
task.completed = !task.completed;
}
}
Explanation:
To better understand the flow and interaction between the components in an MVC architecture, let’s visualize it using a Mermaid.js diagram.
sequenceDiagram participant User participant View participant Controller participant Model User->>View: Interacts with UI View->>Controller: Sends user input Controller->>Model: Updates data Model-->>Controller: Returns updated data Controller->>View: Updates UI View-->>User: Displays updated UI
Diagram Explanation:
To deepen your understanding of MVC, try modifying the examples provided:
The MVC pattern is a powerful tool for structuring applications, providing clear separation of concerns and enhancing maintainability and testability. By understanding and applying MVC in various contexts, from web to mobile to desktop applications, developers can create robust and scalable software solutions. Remember to follow best practices and avoid common pitfalls to make the most of MVC in your projects.