TodoList MVC 极简实现
这是一份来自 Tania Rascia 的 MVC 模式极简实现教程。我们知道像 React、Vue、Angular 这些前端框架都是基于 MVC 设计模式的,虽然都在说 MVC,但你无法在使用上真真切切的感受到严格的 MVC 概念。我在 GitHub 无意间发现了这个用纯 JavaScript 来实现 MVC 的项目,看完源码后大呼过瘾。MVC 中的关系概念被表现的淋漓尽致,如果你也对前端 MVC 模式实现甚至的各类 MVC 变式理解的模模糊糊,那么看完作者的这份源码一定会加深对 MVC 的理解。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Todo App</title>
<style>
*,
*::before,
*::after {
box-sizing: border-box
}
html {
font-family: sans-serif;
font-size: 1rem;
color: #444;
}
#root {
max-width: 450px;
margin: 2rem auto;
padding: 0 1rem;
}
form {
display: flex;
margin-bottom: 2rem;
}
[type="text"],
button {
display: inline-block;
-webkit-appearance: none;
padding: .5rem 1rem;
font-size: 1rem;
border: 2px solid #ccc;
border-radius: 4px;
}
button {
cursor: pointer;
background: #007bff;
color: white;
border: 2px solid #007bff;
margin: 0 .5rem;
}
[type="text"] {
width: 100%;
}
[type="text"]:active,
[type="text"]:focus {
outline: 0;
border: 2px solid #007bff;
}
[type="checkbox"] {
margin-right: 1rem;
font-size: 2rem;
}
h1 {
color: #222;
}
ul {
padding: 0;
}
li {
display: flex;
align-items: center;
padding: 1rem;
margin-bottom: 1rem;
background: #f4f4f4;
border-radius: 4px;
}
li span {
display: inline-block;
padding: .5rem;
width: 250px;
border-radius: 4px;
border: 2px solid transparent;
}
li span:hover {
background: rgba(179, 215, 255, 0.52);
}
li span:focus {
outline: 0;
border: 2px solid #007bff;
background: rgba(179, 207, 255, 0.52)
}
</style>
</head>
<body>
<div id="root"></div>
<script>
/**
* @class Model
*
* Manages the data of the application.
*/
class Model {
constructor() {
this.todos = JSON.parse(localStorage.getItem('todos')) || []
}
bindTodoListChanged(callback) {
this.onTodoListChanged = callback
}
_commit(todos) {
this.onTodoListChanged(todos)
localStorage.setItem('todos', JSON.stringify(todos))
}
addTodo(todoText) {
const todo = {
id: this.todos.length > 0 ? this.todos[this.todos.length - 1].id + 1 : 1,
text: todoText,
complete: false,
}
this.todos.push(todo)
this._commit(this.todos)
}
editTodo(id, updatedText) {
this.todos = this.todos.map(todo =>
todo.id === id ? { id: todo.id, text: updatedText, complete: todo.complete } : todo
)
this._commit(this.todos)
}
deleteTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id)
this._commit(this.todos)
}
toggleTodo(id) {
this.todos = this.todos.map(todo =>
todo.id === id ? { id: todo.id, text: todo.text, complete: !todo.complete } : todo
)
this._commit(this.todos)
}
}
/**
* @class View
*
* Visual representation of the model.
*/
class View {
constructor() {
this.app = this.getElement('#root')
this.form = this.createElement('form')
this.input = this.createElement('input')
this.input.type = 'text'
this.input.placeholder = 'Add todo'
this.input.name = 'todo'
this.submitButton = this.createElement('button')
this.submitButton.textContent = 'Submit'
this.form.append(this.input, this.submitButton)
this.title = this.createElement('h1')
this.title.textContent = 'Todos'
this.todoList = this.createElement('ul', 'todo-list')
this.app.append(this.title, this.form, this.todoList)
this._temporaryTodoText = ''
this._initLocalListeners()
}
get _todoText() {
return this.input.value
}
_resetInput() {
this.input.value = ''
}
createElement(tag, className) {
const element = document.createElement(tag)
if (className) element.classList.add(className)
return element
}
getElement(selector) {
const element = document.querySelector(selector)
return element
}
displayTodos(todos) {
// Delete all nodes
while (this.todoList.firstChild) {
this.todoList.removeChild(this.todoList.firstChild)
}
// Show default message
if (todos.length === 0) {
const p = this.createElement('p')
p.textContent = 'Nothing to do! Add a task?'
this.todoList.append(p)
} else {
// Create nodes
todos.forEach(todo => {
const li = this.createElement('li')
li.id = todo.id
const checkbox = this.createElement('input')
checkbox.type = 'checkbox'
checkbox.checked = todo.complete
const span = this.createElement('span')
span.contentEditable = true
span.classList.add('editable')
if (todo.complete) {
const strike = this.createElement('s')
strike.textContent = todo.text
span.append(strike)
} else {
span.textContent = todo.text
}
const deleteButton = this.createElement('button', 'delete')
deleteButton.textContent = 'Delete'
li.append(checkbox, span, deleteButton)
// Append nodes
this.todoList.append(li)
})
}
// Debugging
console.log(todos)
}
_initLocalListeners() {
this.todoList.addEventListener('input', event => {
if (event.target.className === 'editable') {
this._temporaryTodoText = event.target.innerText
}
})
}
bindAddTodo(handler) {
this.form.addEventListener('submit', event => {
event.preventDefault()
if (this._todoText) {
handler(this._todoText)
this._resetInput()
}
})
}
bindDeleteTodo(handler) {
this.todoList.addEventListener('click', event => {
if (event.target.className === 'delete') {
const id = parseInt(event.target.parentElement.id)
handler(id)
}
})
}
bindEditTodo(handler) {
this.todoList.addEventListener('focusout', event => {
if (this._temporaryTodoText) {
const id = parseInt(event.target.parentElement.id)
handler(id, this._temporaryTodoText)
this._temporaryTodoText = ''
}
})
}
bindToggleTodo(handler) {
this.todoList.addEventListener('change', event => {
if (event.target.type === 'checkbox') {
const id = parseInt(event.target.parentElement.id)
handler(id)
}
})
}
}
/**
* @class Controller
*
* Links the user input and the view output.
*
* @param model
* @param view
*/
class Controller {
constructor(model, view) {
this.model = model
this.view = view
// Explicit this binding
this.model.bindTodoListChanged(this.onTodoListChanged)
this.view.bindAddTodo(this.handleAddTodo)
this.view.bindEditTodo(this.handleEditTodo)
this.view.bindDeleteTodo(this.handleDeleteTodo)
this.view.bindToggleTodo(this.handleToggleTodo)
// Display initial todos
this.onTodoListChanged(this.model.todos)
}
onTodoListChanged = todos => {
this.view.displayTodos(todos)
}
handleAddTodo = todoText => {
this.model.addTodo(todoText)
}
handleEditTodo = (id, todoText) => {
this.model.editTodo(id, todoText)
}
handleDeleteTodo = id => {
this.model.deleteTodo(id)
}
handleToggleTodo = id => {
this.model.toggleTodo(id)
}
}
const app = new Controller(new Model(), new View())
</script>
</body>
</html>
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。