An MVC program in pure JavaScript

Keywords: Javascript React JSON Programming

Author: Tania

Translate: Crazy Technology House

Original: https://www.taniarascia.com/j...

Reproduction is strictly prohibited without permission

I want to use model-view-controller Schema mode writes a simple program in pure JavaScript, so I do it.Hopefully it will help you understand MVC, because it is a difficult concept to understand when you first start to touch it.

I did it This todo application , a simple and compact browser application that allows you to CRUD (create, read, update and delete) your to-dos.It contains only index.html, style.css, and script.js files, which are very simple and do not require any dependencies or frameworks.

Precondition

target

Create a todo application in a browser using pure JavaScript and familiarize yourself with the concepts of MVC (and OOP - object-oriented programming).

Note: Because this program uses the latest JavaScript functionality (ES2017), Babel cannot be compiled into backward compatible JavaScript syntax on some browsers, such as Safari.

What is MVC?

MVC is a very popular pattern for organizational code.

  • Model - Data for Manager
  • View - Visual representation of the model
  • Controller - Link users and systems

The model is the data.In this todo program, this will be the actual to-do and how they will be added, edited, or deleted.

Views are how data is displayed.In this program, it is the HTML presented in DOM and CSS.

Controllers are used to connect models and views.It requires user input, such as clicking or typing, and handles callbacks to user interactions.

The model never touches the view.Views never touch models.Controllers are used to connect them.

I want to mention that MVC for a simple todo program is actually a bunch of templates.If this is the program you want to create and the whole system is created, it really complicates things.The key is to try to understand it at a smaller level.

Initial setup

This will be a program written entirely in JavaScript, which means everything will be handled in JavaScript and HTML will contain only the root element.

index.html

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

    <link rel="stylesheet" href="style.css" />
  </head>

  <body>
    <div id="root"></div>

    <script src="script.js"></script>
  </body>
</html>

I wrote a small amount of CSS just to make it look acceptable, you can find This file And save to style.css.I'm not going to write CSS anymore, because it's not the focus of this article.

Okay, now that we have HTML and CSS, it's time to start writing the program.

Introduction

I'll make this tutorial easy to understand so that you can easily understand which class belongs to which part of MVC.I will create a Model class, a View class, and a Controller class.This program will be an instance of the controller.

If you are unfamiliar with how classes work, read Understanding classes in JavaScript.

class Model {
  constructor() {}
}

class View {
  constructor() {}
}

class Controller {
  constructor(model, view) {
    this.model = model
    this.view = view
  }
}

const app = new Controller(new Model(), new View())

Model

Let's focus on the model first, because it is the simplest of the three parts.It does not involve any events or DOM operations.It simply stores and modifies data.

//Model
class Model {
  constructor() {
    // The state of the model, an array of todo objects, prepopulated with some data
    this.todos = [
      { id: 1, text: 'Run a marathon', complete: false },
      { id: 2, text: 'Plant a garden', complete: false },
    ]
  }

  // Append a todo to the todos array
  addTodo(todo) {
    this.todos = [...this.todos, todo]
  }

  // Map through all todos, and replace the text of the todo with the specified id
  editTodo(id, updatedText) {
    this.todos = this.todos.map(todo =>
      todo.id === id ? { id: todo.id, text: updatedText, complete: todo.complete } : todo
    )
  }

  // Filter a todo out of the array by id
  deleteTodo(id) {
    this.todos = this.todos.filter(todo => todo.id !== id)
  }

  // Flip the complete boolean on the specified todo
  toggleTodo(id) {
    this.todos = this.todos.map(todo =>
      todo.id === id ? { id: todo.id, text: todo.text, complete: !todo.complete } : todo
    )
  }
}

We define addTodo, editTodo, deleteTodo, and toggleTodo.These should all be self-explanatory: add is added to the array, edit finds the id of the todo to edit and replace, delete filters the todo in the array, and toggles the complete Boolean property.

Since we do this in the browser and are accessible from the window (globally), you can easily test these and enter the following:

app.model.addTodo({ id: 3, text: 'Take a nap', complete: false })

A to-do item will be added to the list, and you can view the contents of app.model.todos.

This is sufficient for the current model.Finally, we store to-dos in local storage To make it semi-permanent, but now todo refreshes whenever you refresh the page.

As we can see, the model only processes and modifies the actual data.It does not understand or know what the input - modifying it, or the output - will ultimately display.

If you enter all the operations manually through the console and view the output in the console, you will get everything you need for a fully functional CRUD program.

view

We will create a view by manipulating the DOM - Document Object Model.Without the help of React's JSX or template language, this operation would be lengthy and ugly in normal JavaScript, but it is the essence of manipulating DOM directly.

Controllers and models should not know about DOM, HTML elements, CSS, or anything in it.Anything related to it should be placed in the view.

If you are not familiar with the difference between DOM or DOM and HTML source code, read Introduction to DOM.

The first thing to do is create an auxiliary method to retrieve and create elements.

//view
class View {
  constructor() {}

  // Create an element with an optional CSS class
  createElement(tag, className) {
    const element = document.createElement(tag)
    if (className) element.classList.add(className)

    return element
  }

  // Retrieve an element from the DOM
  getElement(selector) {
    const element = document.querySelector(selector)

    return element
  }
}

It's good so far.Next in the constructor, I'll set everything I need for the view:

  • Root Element of Application - #root
  • Heading h1
  • A form, input box and submit button to add to-dos - form, input, button
  • To-do list - ul

I'll create all the variables in the constructor so they can be easily referenced.

//view
class View {
  constructor() {
    // The root element
    this.app = this.getElement('#root')

    // The title of the app
    this.title = this.createElement('h1')
    this.title.textContent = 'Todos'

    // The form, with a [type="text"] input, and a submit button
    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'

    // The visual representation of the todo list
    this.todoList = this.createElement('ul', 'todo-list')

    // Append the input and submit button to the form
    this.form.append(this.input, this.submitButton)

    // Append the title, form, and todo list to the app
    this.app.append(this.title, this.form, this.todoList)
  }
  // ...
}

Now, you'll set the view part that won't be changed.

There are two other little things: getter s and resetter s that input (new todo) values.

// view
get todoText() {
  return this.input.value
}

resetInput() {
  this.input.value = ''
}

All settings are now complete.The most complex part is displaying a to-do list, which is the part that will be changed each time a to-do is modified.

//view
displayTodos(todos) {
  // ...
}

The displayTodos method creates the ul and li contained in the to-do list and displays them.Each time a todo is modified, added, or deleted, the displayTodos method is called again using the todos in the model, resetting the list, and displaying them again.This keeps the view synchronized with the state of the model.

The first thing we do is delete all todo nodes each time we call.Then check if there are to-dos.If we don't, we'll get an empty list message.

// view
// 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 {
  // ...
}

Now iterate through to-dos and display check boxes, span s, and delete buttons for each existing to-do.

// view
else {
  // Create todo item nodes for each todo in state
  todos.forEach(todo => {
    const li = this.createElement('li')
    li.id = todo.id

    // Each todo item will have a checkbox you can toggle
    const checkbox = this.createElement('input')
    checkbox.type = 'checkbox'
    checkbox.checked = todo.complete

    // The todo item text will be in a contenteditable span
    const span = this.createElement('span')
    span.contentEditable = true
    span.classList.add('editable')

    // If the todo is complete, it will have a strikethrough
    if (todo.complete) {
      const strike = this.createElement('s')
      strike.textContent = todo.text
      span.append(strike)
    } else {
      // Otherwise just display the text
      span.textContent = todo.text
    }

    // The todos will also have a delete button
    const deleteButton = this.createElement('button', 'delete')
    deleteButton.textContent = 'Delete'
    li.append(checkbox, span, deleteButton)

    // Append nodes to the todo list
    this.todoList.append(li)
  })
}

Now set up the view and model.We just can't connect them because there are no event monitors for user input and no handle for output that handles such events.

The console still exists as a temporary controller through which you can add and delete to-dos.

Controller

Finally, the controller is the link between the model (data) and the view (what the user sees).That's what we've been doing in our controller so far.

//Controller
class Controller {
  constructor(model, view) {
    this.model = model
    this.view = view
  }
}

The first link between the view and the model is to create a method that calls displayTodos each time a todo changes.We can also call it once in the constructor to display the initial todos, if any.

//Controller
class Controller {
  constructor(model, view) {
    this.model = model
    this.view = view

    // Display initial todos
    this.onTodoListChanged(this.model.todos)
  }

  onTodoListChanged = todos => {
    this.view.displayTodos(todos)
  }
}

The controller will trigger the post-processing event.An event is triggered when you submit a new to-do, click the Delete button, or click the check box for the to-do.The view must listen for these events because they are user input to the view, which assigns to the controller the work to do in response to events.

We will create handler s for events.First, submit a handleAddTodo event, which can be triggered by pressing the Enter key or clicking the Submit button when the to-do input form we created is submitted.This is a submit event.

Back in the view, we'll use this.input.value's getter as get todoText.To ensure that the input cannot be empty, we will then create a todo with an id, text, and a complete value of false.Add todo to the model and reset the input box.

// Controller
// Handle submit event for adding a todo
handleAddTodo = event => {
  event.preventDefault()

  if (this.view.todoText) {
    const todo = {
      id: this.model.todos.length > 0 ? this.model.todos[this.model.todos.length - 1].id + 1 : 1,
      text: this.view.todoText,
      complete: false,
    }

    this.model.addTodo(todo)
    this.view.resetInput()
  }
}

Deleting todo is similar.It will respond to the click event on the delete button.The parent element of the delete button is the todo li itself, with the corresponding id attached.We need to send this data to the correct model method.

// Controller
// Handle click event for deleting a todo
handleDeleteTodo = event => {
  if (event.target.className === 'delete') {
    const id = parseInt(event.target.parentElement.id)

    this.model.deleteTodo(id)
  }
}

In JavaScript, a change event is raised when you click the check box to toggle it.Processing this method as if you were clicking the Delete button, and calling the model method.

// Controller
// Handle change event for toggling a todo
handleToggle = event => {
  if (event.target.type === 'checkbox') {
    const id = parseInt(event.target.parentElement.id)

    this.model.toggleTodo(id)
  }
}

These controller methods are a bit messy - ideally they should not handle any logic, but should simply call the model.

Set up event listeners

Now we have these three handler s, but the controller still doesn't know when to call them.Event listeners must be placed on DOM elements in the view.We will reply to the submit event on the form, as well as the click and change events on the todo list.

Add a bindEvents method to the View that will call these events.

// view
bindEvents(controller) {
  this.form.addEventListener('submit', controller.handleAddTodo)
  this.todoList.addEventListener('click', controller.handleDeleteTodo)
  this.todoList.addEventListener('change', controller.handleToggle)
}

Next, bind the method to listen for events to the view.In Controller's constructor, call bindEvents and pass the controller's this context.

Arrow functions are used on all handle events.This allows us to call the controllers from the view using their this context.If we don't use arrow functions, we'll have to bind them manually, such as controller.handleAddTodo.bind(this).

// Controller
this.view.bindEvents(this)

Now, when a submit, click, or change event occurs on the specified element, the corresponding handler will be called.

Callbacks in response models

We also missed something: the event was listening, the handler was called, but nothing happened.This is because the model does not know how the view should be updated and how to update it.We have a displayTodos method on the view to solve this problem, but as mentioned earlier, models and views should not understand each other.

Just like listening for events, the model should go back to the controller and let it know what happened.

We've created the onTodoListChanged method on the controller to handle this problem, and now we just need to let the model know about it.We bind it to the model, just as we did to the handler on the view.

In the model, add bindEvents for onTodoListChanged.

// Model
bindEvents(controller) {
  this.onTodoListChanged = controller.onTodoListChanged
}

In the controller, send this context.

// Controller
constructor() {
  // ...
  this.model.bindEvents(this)
  this.view.bindEvents(this)
}

Now, after each method in the model, you will call the onTodoListChanged callback.

In more complex programs, there may be different callbacks for different events, but in this simple to-do program, we can share a callback between all methods.

//Model
addTodo(todo) {
  this.todos = [...this.todos, todo]

  this.onTodoListChanged(this.todos)
}

Add local storage

Most of the program is now complete and all the concepts have been demonstrated.We can persist the data by saving it in the browser's local store.

If you don't know how local storage works, read How to use JavaScript local storage.

Now we can set the initial value of the to-do to either a local storage or an empty array.

// Model
class Model {
  constructor() {
    this.todos = JSON.parse(localStorage.getItem('todos')) || []
  }
}

Then create an update function to update the value of the localStorage.

//Model
update() {
  localStorage.setItem('todos', JSON.stringify(this.todos))
}

We can call this.todos every time we change it.

//Model
addTodo(todo) {
  this.todos = [...this.todos, todo]
  this.update()

  this.onTodoListChanged(this.todos)
}

Add Live Editing

The last part of the puzzle is the ability to edit existing to-dos.Editing is always more difficult than adding or deleting.I want to simplify it without editing buttons or replacing span with input or anything.We also don't want to call editTodo for every letter entered, because it will re-render the entire to-do list UI.

I decided to create a method on the controller that updates the temporary state variable with a new edit value, and another method that calls the editTodo method in the model.

//Controller
constructor() {
  // ...
  this.temporaryEditValue
}

// Update temporary state
handleEditTodo = event => {
  if (event.target.className === 'editable') {
    this.temporaryEditValue = event.target.innerText
  }
}

// Send the completed value to the model
handleEditTodoComplete = event => {
  if (this.temporaryEditValue) {
    const id = parseInt(event.target.parentElement.id)

    this.model.editTodo(id, this.temporaryEditValue)
    this.temporaryEditValue = ''
  }
}

I admit that this solution is a bit confusing because the temporaryEditValue variable should technically be in the view, not in the controller, because it is a view-related state.

Now we can add these to the event listener for the view.When you enter a contenteditable element, the input event is triggered, and when you leave the contenteditable element, the focusout triggers.

//view
bindEvents(controller) {
  this.form.addEventListener('submit', controller.handleAddTodo)
  this.todoList.addEventListener('click', controller.handleDeleteTodo)
  this.todoList.addEventListener('input', controller.handleEditTodo)
  this.todoList.addEventListener('focusout', controller.handleEditTodoComplete)
  this.todoList.addEventListener('change', controller.handleToggle)
}

Now, when you click any to-do, you enter Edit mode, which updates the temporary state variable, which is saved in the model and resets the temporary state when you select or click to-do.

The contenteditable solution was soon implemented.There are a variety of issues to consider when using contenteditable in your programs. I've written a lot here.

summary

Now you have a todo program written in pure JavaScript that demonstrates the concept of a Model-View-Controller architecture.Below is a link to the demo and source code.

I hope this tutorial will help you understand MVC.Using this loosely coupled pattern can add a lot of templates and abstractions to your program, but it is also a familiar pattern for developers and an important concept that is often used in many frameworks.

WeChat Public Number for the First Issue of this Article: A Pioneer

Welcome to Scan QR Code for Public Numbers and push you fresh front-end technical articles every day

Welcome to continue reading other great articles in this column:

Posted by magi on Wed, 07 Aug 2019 21:21:05 -0700