TwitterFacebookInstagramYouTubeDEV CommunityGitHub
MVC Architectural Design Pattern With Vanilla JS

MVC Architectural Design Pattern With Vanilla JS

Most people assume that the meaning of MVC (Model-View-Controller) is static, but that's simply not true. MVC is not a well-defined concept, so do not assume it to be one in conversations. Instead, treat it more like a buzzword like "cloud".😉 MVC takes on a slightly different meaning depending on the specific context. Without a context, it is only a "fuzzy" concept in general.

MVC is one of the most well-known architectural patterns and is the basis of many JavaScript frameworks (such as Angular) and several server-rendered web frameworks (such as ASP.NET Core MVC). As suggested by the post title, in this article, we will discuss the MVC pattern in the context of client-side development using JavaScript.

In this article, we will:

  • briefly explore the original meaning of the MVC pattern by looking at its history
  • explore the meaning of the MVC pattern in the context of JavaScript frontend development
  • implement the MVC architectural pattern using only Vanilla JS on the frontend to gain a deep understanding and hands-on experience with this design pattern

History of MVC

The MVC pattern was invented by Trygve Reenskaug while he was a visiting scientist at the Smalltalk group at the Xerox Palo Alto Research Center (PARC). He wrote his first paper on MVC in 1978. The MVC pattern was invented long before the first web browser and was first implemented as part of the Smalltalk-80 class library. It was originally used as an architectural pattern for creating graphical user interfaces (GUIs).

The original meaning of the Model-View-Controller pattern was very different than the meaning associated with the pattern today. In the context of a GUI,  the Model View Controller pattern was interpreted as follows:

  • Model: A particular piece of data represented by an application, e.g. weather station temperature reading.
  • View: One representation of data from the model. The same model might have multiple views associated with it. The views are associated with a particular model through the Observer relationship. For example, a temperature reading might be represented by both a label and a bar chart.
  • Controller: Collects user inputs and modifies the model, e.g. the controller might collect mouse click events and update the model.

The primary benefit of this original version of the MVC pattern is a Separated Presentation, which was defined by Martin Fowler as:

A pattern that is used to ensure that any code that manipulates presentation only manipulates representation, pushing all domain and data source logic into clearly separated areas of the program.

In this pattern, the view is updated indirectly from the model via event handling. The controller does not interact directly with the view. Instead, the controller modifies the model, and since the view is observing the model, the view gets updated.

Why MVC?

To really appreciate the beauty of the MVC pattern, let's look at an example of code without MVC.

The traditional server-rendered web pages consist of static HTML documents. Each time the user requests a page, the server returns a new HTML page with the state of the information on the server at that moment in time. Then, AJAX came along allowing us to update page elements and send user requests back to the server without having to wait for the server. However, AJAX development and adoption also brought with it more complexity in our code that required some sort of code restructuring for separation of concern, modularity, and reusability.

Consider the following example. We have a simple form with a single textbox to allow the user to enter their email address for account registration. We will need to set up some validation logic for our form. In this case, the event handler function for the Submit button in the script loops through a predetermined list of fields and run through the validation logic for each of the fields based on the class name metadata. Go ahead and try to click Submit without any email address entered. You should see an alert dialog pop up with the warning.👇

Although this approach works, it isn't very flexible. What if we want to add fields or validate a different form on another page. We would have to duplicate most of this functionality for each new field we add. Also, what if our validation logic is much more complicated than this, e.g. a field is required only if another field is completed? You can probably imagine that our code would eventually grow out of control.😵

MVC with Vanilla JS

Most of you are familiar with Single-Page Applications (SPA) MVC frameworks such as Angular that have become increasingly popular over the past few years. However, it's possible to implement the MVC architectural pattern on the front-end without using a framework or library. One of the benefits of this exercise is that it provides you with a solid understanding of how this pattern is implemented "under the hood" so that when it's time for you to pick up a framework, you feel more comfortable understanding why it works the way it does. Without further ado, let's get started!😎

Build the View, Model, Controller

Before we dive into the code, below is a demo of what we will be building for this example. Try clicking on the word "Hello" and observe how the text is dynamically updated to "World". Feel free to look at the files to familiarize yourself with the MVC structure.👇

In the context of our frontend JS application, MVC is an architecture with three layers:

  • Model: is the container of our state and also performs business/domain logic.
  • View: is the visual representation of the models, i.e. the output displayed to the user
  • Controller: serves as the mediator between the model and the view and also stores some non-domain application states.

The View

For a browser-based, vanilla JS implementation, we have HTML and the DOM to worry about, so there will be two pieces to our view: the HTML itself and a View class that encapsulates relevant DOM selectors.

In our example, the HTML is very simple. It has the <h1> element with the id=heading, and references several scripts containing all of our code for the model, view, and controller. The initial value for the <h1> element's innerText will be set by the Controller as you shall see later.

<html>
  <head>
    <meta charset=”utf-8">
    <meta name="”viewport”" content="”width" ="device-width”" />
    <title>MVC Experiment</title>
    <script src="./src/index.js"></script>
    <script src="./controllers/heading.controller.js"></script>
    <script src="./models/heading.model.js"></script>
    <script src="./views/heading.view.js"></script>
  </head>

  <body>
    <h1 id="heading"></h1>
  </body>
</html>

Our View class is defined in the heading.view.js file. We're passing a controller into the constructor as a dependency. The controller will be listening for events on our relevant DOM nodes. Here, we're interested in the <h1> DOM node with the id=heading. So, we populate the initial value for the innerText of this <h1> element to the initial state of the model, and we do that via the controller.

We also register the event handler for the <h1> element via the addEventListener() method of the EventTarget interface available in the Web API. This interface can be implemented by any object that can receive events such as element, document, window, etc. The addEventListener() method sets up a function that will be called whenever the specified event is delivered by the target. In this case, we're passing in click as the event type to listen for and the controller itself as an object that implements the EventListerner interface as you shall see in the section about the controller.

class HeadingView {
  constructor(controller) {
    this.controller = controller;
    this.heading = document.getElementById("heading");
    this.heading.innerText = controller.modelHeading;
    this.heading.addEventListener("click", controller);
  }
}

export { HeadingView };

The Model

Our model is defined in the heading.model.js file. For this example, our model will be very simple. It consists of a value for the heading, which is what we used to set our <h1> element's innerText. The model will be injected into the controller so that the controller can manipulate the model as prompted by the view's events.

class HeadingModel {
  constructor() {
    this.heading = "Hello";
  }
}

export { HeadingModel };

The Controller

As mentioned in the section about the View, in order for the event handler to work as expected, the controller must implement the EventListener interface of the Web API. This interface represents an object that can handle an event dispatched by an EventTarget object (in this case, it's the <h1> element). Any object that uses this interface (in this case, it's the controller) must implement the handleEvent method, which is a function that is called whenever an event of the specified type occurs.

Our controller is defined in the heading.controller.js file.

class HeadingController {
  constructor(model) {
    this.model = model;
  }

  //EVENTLISTENER INTERFACE
  handleEvent(e) {
    e.stopPropagation();
    switch (e.type) {
      case "click":
        this.clickHandler(e.target);
        break;
      default:
        console.log(e.target);
    }
  }

  get modelHeading() {
    return this.model.heading;
  }

  //CHANGE THE MODEL
  clickHandler(target) {
    this.model.heading = "World";
    target.innerText = this.modelHeading;
  }
}

export { HeadingController };

As shown in the code above, our controller implements three methods:

  • handleEvent: This method handles the click event for the <h1> element. If the event is click then the clickHandler() method is called.
  • clickHandler: This method updates the model as well as the <h1> element's innerText (represented as the target parameter).
  • getModelHeading: This method is used to retrieve the current value of the heading property of the model. As mentioned in the View section, we call this method to get the initial state of the model and assign the returned value to the <h1> element.

Wiring Up Model-View-Controller

Now that we have our model, view, and controller defined, we need to create and inject them in the correct order. We'll do that in the index.js file.

import { HeadingModel } from "../models/heading.model";
import { HeadingController } from "../controllers/heading.controller";
import { HeadingView } from "../views/heading.view";

function main() {
  var model = new HeadingModel();
  var controller = new HeadingController(model);
  var view = new HeadingView(controller);
}

main();

The model is instantiated first, injected into the constructor for the controller, and then the controller is injected into the view. Our model does not know anything about the view and vice versa. The controller can interact with both since it's injected into the view, and the model is injected into the controller. Finally, we invoke the main() function and we should be able to click "Hello" to change it to "World".

And there you have it. We've learned how to structure our code following the MVC pattern using just Vanilla JS. Sure, we ended up with more code than what we had in the example without using MVC, but the idea is that this pattern can be expanded to make your code more modular and maintainable as your application grows in its complexity.

We can stop here, but I think we can expand on these building blocks to make it even better by implementing the Observer pattern to achieve a unidirectional data flow between the model and the view. We can also add the ability to toggle between "Hello" and "World" by implementing the State pattern. Going through these exercises would also help us learn about these two design patterns (Observer and State patterns) in the most practical way. So let's do that in the next two articles.💪