UI Patterns: Understanding the Basics of MVC

av paul

Last Updated on Thu, Sep 05, 2019

07 mins read

In the world of software development, patterns are everywhere. The Model-View-Controller is one of the widely recognized presentation patterns you should understand πŸ‘Œ

Goal

In this article, we are going to implement a simple MVC app with pure javascript to understand how it works.

MVC is a presentational pattern commonly used for developing UIs. It is also referred to as a software design pattern. I used presentational to make clear it's context: UI. MVC has three components and each of them has a specific function:

  • Model: directly manages data, logic, and rules of the application
  • View: presentation of the model
  • Controller: responds to user input and converts it to commands for the model/view

Show me the codeγ€ˆ/〉

The app we are going to build, allows you to record all your transactions in one place 😜 For now, we will implement the save transaction functionality, the challenge to improve it is on you. We will use basic HTML, CSS, and Javascript(ES6).

Initial setup

Our app is fully Javascript, everything is handled through Javascript. The HTML will contain the root element that our app will use to dynamically display(render) other UI elements.

<!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>Tippy</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <noscript>You need to enable javascript to run this app</noscript>
    <div id="root">
      <!-- this is the root element of the app 
        other elements will be displayed inside here -->
    </div>
    <!-- import the script running the app -->
    <script src="index.js" ></script>
  </body>
</html>

To make our app look nice and pretty, lets add some CSS. We will use Bootstrap and some custom CSS. reminder: Bootstrap was added in the HTML file.

body,
html {
  width: 100%;
}

#root {
  max-width: 45%;
  margin: 0 auto;
}

h1 {
  margin-top: 2rem;
}

select.form-control,
input.form-control {
  width: 47%;
  display: inline-block;
}

.list-group-item .description {
  font-style: italic;
}

.list-group-item .amount {
  font-size: 1.2rem;
  color: #28a745;
}

.list-group-item .amount.expense {
  color: #dc3545;
}

.list-group-item .description::first-letter {
  text-transform: capitalize;
}

Now that we have the basics we are ready to jump into the waters πŸŠβ€β™‚οΈ

Getting started

Our index.js file will have three classes corresponding to the three MVC components. The app will be an instance of the controller class.

You can learn more about Javascript classes on MDN

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: managing data

The model is the simplest component in MVC, no events or DOM manipulation. It stores the data and knows how to modify it.

class Model {
  constructor() {
     // ids to help us if we want to edit or delete transactions 
    this.id = 0;
    // initial transactions for presentation
    this.transactions = [
      {
        id: ++this.id,
        type: "income",
        amount: 5700,
        comment: "freelancing income for August 2019"
      },
      {
        id: ++this.id,
        type: "expense",
        amount: 100,
        comment: "data monthly subscription"
      }
    ];
  }

// function to add a new transaction
addTransaction({ type, amount, comment }) {
    const transaction = { id: ++this.id, type, amount, comment };
    this.transactions.push(transaction);
  }
}

Very short and simple, yes πŸ‘

View: model presentation

Our Model is storing data, we need to display the data to our users. The View handles all DOM manipulations, that's why it's very verbose 😁

class View {
  constructor() {
    // create all initial elements that make up the UI and add them in the root element

    // create element takes the element name and optional class to add
    // all classnames added are from Bootstrap
    this.app = this.getElement("#root");
    this.title = this.createElement("h1");
    this.title.innerText = "Your Transactions";
    this.form = this.createElement("form", "my-3");

    this.amountInput = this.createElement("input", "form-control my-2 mr-3");
    this.amountInput.setAttribute("type", "number");
    this.amountInput.setAttribute("placeholder", "Amount");

    this.typeInput = this.createElement("select", "form-control my-2 ml-3");
    const opt1 = this.createElement("option");
    opt1.setAttribute("value", "income");
    opt1.innerText = "Income";
    const opt2 = this.createElement("option");
    opt2.setAttribute("value", "expense");
    opt2.innerText = "Expense";
    this.typeInput.append(opt1, opt2);

    this.commentInput = this.createElement("textarea", "form-control my-2");
    this.commentInput.setAttribute("placeholder", "Description");

    this.submitInputs = this.createElement("button", "btn btn-primary my-2");
    this.submitInputs.innerText = "Save";

    this.form.append(
      this.amountInput,
      this.typeInput,
      this.commentInput,
      this.submitInputs
    );

    this.transactionsList = this.createElement("list", "list-group");

    this.app.append(this.title, this.form, this.transactionsList);
  }

  // helper method to create an element with optional className
  createElement(tag, classname) {
    const element = document.createElement(tag);
    if (classname) element.setAttribute("class", classname);
    return element;
  }

  //helper method to get an element
  getElement(selector) {
    const element = document.querySelector(selector);
    return element;
  }
}

The complex part of the view is displaying data, let's add another function in the view class to handle that task.

// inside View class

// display transaction list
displayTransactions(transactions) {
  // delete all nodes if there is any, to make DOM manipulations easy
  while (this.transactionsList.firstChild) {
    this.transactionsList.removeChild(this.transactionsList.firstChild);
  }

  // show the default message if list empty
  if (transactions.length === 0) {
    const message = this.createElement("p");
    message.innerText = "Don't forget to record your transactions!";
  } else {
    transactions.forEach(transaction => {
      const amount = this.createElement("span", "amount " + transaction.type);

      if (transaction.type === "expense") {
        amount.innerText = "-" + transaction.amount;
      } else {
        amount.innerText = transaction.amount;
      }

      const comment = this.createElement("p", "description");
      comment.innerText = transaction.comment;
      const container = this.createElement("div", "list-group-item");
      container.append(amount, comment);
      this.transactionsList.append(container);
    });
  }
}

Still simple! Yes 😊

Controller: inputs and interactions

Now we have our data stored in the Model, we also have the View ready to display the data. We need to bring both parties together. In MVC, the model doesn't know the existence of the View and vice versa. The Controller is the only component aware of their existence. reminder: our app is the instance of the controller.

// in the index.js file together with the Model and the View classes
class Controller {
  constructor(model, view) {
    this.model = model;
    this.view = view;
    // display the initial model transactions
    this.onTransactionChange(model.transactions);
  }

  // handles Model changes
  onTransactionChange = transactionsList => {
    this.view.displayTransactions(transactionsList);
  };
}

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

Now our app looks like this πŸ‘‡πŸ‘πŸ‘πŸ‘ How our app looks by here If you try to add a new record, it reloads the page and the model stays the same πŸ˜• Remember that the controller's function is to accept input and convert it to commands for the model or the view? Although the View listens to events it doesn't know what to do with them. They must be handled by the controller. Let's tell the View to call the controller when we click on save button.

// in the View class add
// get transaction data from form inputs
  getTransactionData() {
    const type = this.typeInput.value;
    const amount = this.amountInput.value;
    const comment = this.commentInput.value;

    return { type, amount, comment };
  }

// reset inputs
  resetInputs() {
    this.typeInput.firstChild.setAttribute("selected", "");
    this.amountInput.value = null;
    this.commentInput.value = "";
  }

// used by the controller to bind the handler to the event
bindAddTransaction(handler) {
  this.form.addEventListener("submit", evt => {
    evt.preventDefault();
    const transaction = this.getTransactionData();
    handler(transaction);
    this.resetInputs();
  });
}

We also need to tell the controller what to do when the View submit action is triggered and bind the event handler to the View.

// in the Controller class
constructor(model, view) {
...
  this.view.bindAddTransaction(this.handleAddTransaction);
...
}

handleAddTransaction = transaction => {
  this.model.addTransaction(transaction);
};

Now we are updating our Model πŸ‘ But the View is not reflecting the latest transactions model. What are we forgetting? πŸ€” If you followed very well, you know that the Model doesn't know the existence of the View! We need to let the View know that the transactions model was updated.

// in the Model class
// this function is used by the controller to bind a handler to the model change
bindTransactionChanged(handler) {
  this.onTransactionsChanged = handler;
}

// update the `addTransaction` method
addTransaction({ type, amount, comment }) {
  const transaction = { type, amount, comment };
  this.transactions.push(transaction);
  this.onTransactionsChanged(this.transactions);
}

While in the controller:

// update the constructor with this line
constructor(model, view) {
...
  this.model.bindTransactionChanged(this.onTransactionChange);
...
}

// model transaction change handler
handleAddTransaction = transaction => {
  this.model.addTransaction(transaction);
};

Now our MVC app is working! You can add transactions and it's ready for improvements. Challenge yourself to add other functionalities like delete and edit. You understand how all the three components are connected. Now you know the MVC pattern 😎

Modern MVC:

After reading too much about the pattern and using it in real-world projects, I find MVC to be more of a concept as explained here. MVC is a concept that centers around the separation of concerns. Decouple your UI into different sections to handle different responsibilities. That's why modern frameworks are not MVC out of the box! What they all share is the MVC concept separation of concepts.

Thanks for taking the time to read and reach this far πŸ‘ I would like to hear what you think on Twitter.

Β  back home