TodoList MVC 极简实现

这是一份来自 Tania Rascia 的 MVC 模式极简实现教程。我们知道像 React、Vue、Angular 这些前端框架都是基于 MVC 设计模式的,虽然都在说 MVC,但你无法在使用上真真切切的感受到严格的 MVC 概念。我在 GitHub 无意间发现了这个用纯 JavaScript 来实现 MVC 的项目,看完源码后大呼过瘾。MVC 中的关系概念被表现的淋漓尽致,如果你也对前端 MVC 模式实现甚至的各类 MVC 变式理解的模模糊糊,那么看完作者的这份源码一定会加深对 MVC 的理解。

mvcPattern.png

<!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>

原教程地址:https://www.taniarascia.com/javascript-mvc-todo-app/


hiscc
276 声望13 粉丝

萌面大盗与阿里巴巴:-)