JavaScript - - By Camilo Reyes

The MVC Design Pattern in Vanilla JavaScript

A developer with a laptop in front of the Hollywood sign - The MVC design pattern

Design patterns often get incorporated into popular frameworks. The Model-View-Controller (MVC) design pattern, for example, is one that is ubiquitous. In JavaScript, it is hard to decouple the framework from the design pattern. Oftentimes, a particular framework will come with its own interpretation of this design pattern. Frameworks come with opinions and each one forces you to think in a certain way.

Modern frameworks dictate what the concrete implementation of the MVC pattern looks like. This is confusing when all interpretations are different, which adds noise and chaos. When any code base adopts more than one framework it creates a frustrating mess. The question in my mind is, is there a better way?

The MVC pattern is good for client-side frameworks, but modern frameworks change. What is modern today is subject to the passing of time and withers away. In this take, I’d like to explore alternatives and see where a little discipline takes us.

By itself, the MVC pattern dates back more than a few of decades. This makes it a good design pattern to invest your programming skills in. The MVC pattern is a design pattern that can stand on its own. The question is, how far can this take us?

Wait, Is This Yet Another Framework?

First, I’d like to dispel this common myth: a design pattern is not a framework. A design pattern is a disciplined approach to solving a code problem. There is a level of skill necessary and places the responsibility on the programmer. A design pattern separates concerns and promotes clean code.

A framework is different since it does not have to adhere to any design pattern. A way to tell a framework from a pattern is to look for the Hollywood principle. The Hollywood principle is, “don’t call us, we’ll call you.” Anytime there is a dependency that dictates when you use it, it is a framework. A framework is a lot like Hollywood in that you don’t get a say on what to do or how to do it. In fact, developers are like actors because they follow the script when asked to act.

There are good reasons to avoid client-side frameworks:

  • Frameworks add complexity and risk to your solution
  • You experience dependency lock-in, which leads to unmaintainable code
  • As new fad frameworks emerge, it is difficult to rewrite existing legacy code

The MVC Pattern

The MVC design pattern emerged from the Xerox Smalltalk research project in the 1970s and into the 80s. It is a pattern that has stood the test of time for front-end graphical user interfaces. The pattern came from desktop applications but has proven to be effective for web apps too.

At its crux, the MVC design pattern is about a clean cut separation of concerns. The idea is to make the solution intelligible and inviting. Any fellow programmer looking to make specific changes can find the right spot with ease.

Read Modern JavaScript

Keep up-to-date with the evolving world of JavaScript

A Demo of Penguins

Penguins! Cute and cuddly, some of the furriest creatures on the planet. So cute, in fact, there are 17 different kinds of penguins which don’t all live in Antarctic conditions.

Time for a demo of penguins! I’ll show a deck showing several species on a single page. For this, I’d like to use the MVC design pattern and a little discipline. I’ll use the extreme programming methodology to solve the problem at hand with unit tests and no nonsense. At the end, you should be able to flip through a few penguins each with its own data and profile picture.

By the end of this example, you should have learned enough to use the MVC design pattern in plain JavaScript. The pattern itself is super testable so expect good unit tests.

I’ll stick to ES5 for this demo for cross-browser compatibility reasons. It makes sense to use proven language features with this perennial design pattern.

Are you ready? Let’s find out.

The skeleton

The demo will consist of three main parts: controller, view, and model. Each with its own concern and problem that it needs to solve.

A visual of what this looks like is below:

Demo of Penguins Visual

The PenguinController handles events and is the mediator between the view and model. It works out what happens when the user performs an action (for example, clicking on a button or pressing a key). Client-side specific logic can go in the controller. In a bigger system, where there is a lot going on, you can break it out into modules. The controller is the entry point for events and the only mediator between the view and data.

The PenguinView cares about the DOM. The DOM is the browser API you use to make HTML manipulations. In MVC, no other part cares about changing the DOM except for the view. The view can attach user events but leaves event handling concerns to the controller. The view’s prime directive is to change the state of what the user sees on the screen. For this demo, the view will do the DOM manipulations in plain JavaScript.

The PenguinModel cares about data. In client-side JavaScript, this means Ajax. One advantage of the MVC pattern is you now have a single place for server-side Ajax calls. This makes it inviting for fellow programmers who are not familiar with the solution. The model in this design pattern cares only about JSON or objects that come from the server.

One anti-pattern is to violate this intrinsic separation of concerns. The model, for example, must not care about HTML. The view must not care about Ajax. The controller must serve as the mediator without worrying about implementation details.

What I find with this pattern is developers start with good intentions but leak concerns. It is tempting to turn everything into a web component and end up with mush. The emphasis gets put on features and user facing concerns. But, feature concerns are not the same as functional concerns.

In programming, what I like is having a clean cut separation of functional concerns. Each separate programming problem gets a consistent way of solving it. This makes it a lot more intelligible when you read the code. The idea is to write code that is inviting so others can make positive contributions too.

It would not be much of a demo without a real live example you can see and touch. So, without further ado, below is a CodePen showcasing the demo of penguins:

See the Pen A Demo of Penguins by SitePoint (@SitePoint) on CodePen.

Enough talk, time for some code.

The controller

The view and model are two components used by the controller. The controller has in its constructor all the components it needs to do the job:

var PenguinController = function PenguinController(penguinView, penguinModel) {
  this.penguinView = penguinView;
  this.penguinModel = penguinModel;
};

The constructor uses inversion of control and injects modules in this way. This pattern enables you to inject any component that meets the high-level contract. Think of it as a nice way to abstract code from implementation details. This pattern empowers you to write clean code in plain JavaScript.

Then, user events get wired up and handled in this way:

PenguinController.prototype.initialize = function initialize() {
  this.penguinView.onClickGetPenguin = this.onClickGetPenguin.bind(this);
};

PenguinController.prototype.onClickGetPenguin = function onClickGetPenguin(e) {
  var target = e.currentTarget;
  var index = parseInt(target.dataset.penguinIndex, 10);

  this.penguinModel.getPenguin(index, this.showPenguin.bind(this));
};

Note this event uses the current target to grab the state stored in the DOM. The DOM, in this case, tells you everything you need to know about its current state. The current state of the DOM is what users see on the browser. You can store state data in the DOM itself, as long as the controller does not change state.

When an event is fired, the controller grabs the data and says what happens next. The this.showPenguin() callback is of interest:

PenguinController.prototype.showPenguin = function showPenguin(penguinModelData) {
  var penguinViewModel = {
    name: penguinModelData.name,
    imageUrl: penguinModelData.imageUrl,
    size: penguinModelData.size,
    favoriteFood: penguinModelData.favoriteFood
  };

  penguinViewModel.previousIndex = penguinModelData.index - 1;
  penguinViewModel.nextIndex = penguinModelData.index + 1;

  if (penguinModelData.index === 0) {
    penguinViewModel.previousIndex = penguinModelData.count - 1;
  }

  if (penguinModelData.index === penguinModelData.count - 1) {
    penguinViewModel.nextIndex = 0;
  }

  this.penguinView.render(penguinViewModel);
};

The controller calculates indexes for each penguin and tells the view to render this. It grabs data from the model and transforms it into an object the view understands and cares about.

Here is a unit test of the happy path when showing a penguin:

var PenguinViewMock = function PenguinViewMock() {
  this.calledRenderWith = null;
};

PenguinViewMock.prototype.render = function render(penguinViewModel) {
  this.calledRenderWith = penguinViewModel;
};

// Arrange
var penguinViewMock = new PenguinViewMock();

var controller = new PenguinController(penguinViewMock, null);

var penguinModelData = {
  name: 'Chinstrap',
  imageUrl: 'http://chinstrapl.jpg',
  size: '5.0kg (m), 4.8kg (f)',
  favoriteFood: 'krill',
  index: 2,
  count: 5
};

// Act
controller.showPenguin(penguinModelData);

// Assert
assert.strictEqual(penguinViewMock.calledRenderWith.name, 'Chinstrap');
assert.strictEqual(penguinViewMock.calledRenderWith.imageUrl, 'http://chinstrapl.jpg');
assert.strictEqual(penguinViewMock.calledRenderWith.size, '5.0kg (m), 4.8kg (f)');
assert.strictEqual(penguinViewMock.calledRenderWith.favoriteFood, 'krill');
assert.strictEqual(penguinViewMock.calledRenderWith.previousIndex, 1);
assert.strictEqual(penguinViewMock.calledRenderWith.nextIndex, 3);

The PenguinViewMock has the same contract that the real implementation has. This makes it possible to write unit tests and make assertions. The assert comes from Node assertions, and is also available in Chai assertions. This enables you to write tests that can run both on Node and on the browser.

Note the controller does not care about implementation details. It uses the contracts the view provides like this.render(). This is the discipline necessary for clean code. The controller can trust each component to do what it says it will do. This adds transparency which makes the code readable.

The view

The view only cares about the DOM element and wiring up events, for example:

var PenguinView = function PenguinView(element) {
  this.element = element;

  this.onClickGetPenguin = null;
};

When it changes the state of what the user sees, the implementation looks like this:

PenguinView.prototype.render = function render(viewModel) {
  this.element.innerHTML = '<h3>' + viewModel.name + '</h3>' +
    '<img class="penguin-image" src="' + viewModel.imageUrl +
      '" alt="' + viewModel.name + '" />' +
    '<p><b>Size:</b> ' + viewModel.size + '</p>' +
    '<p><b>Favorite food:</b> ' + viewModel.favoriteFood + '</p>' +
    '<a id="previousPenguin" class="previous button" href="javascript:void(0);"' +
      ' data-penguin-index="' + viewModel.previousIndex + '">Previous</a> ' +
    '<a id="nextPenguin" class="next button" href="javascript:void(0);"' +
      ' data-penguin-index="' + viewModel.nextIndex + '">Next</a>';

  this.previousIndex = viewModel.previousIndex;
  this.nextIndex = viewModel.nextIndex;

  // Wire up click events, and let the controller handle events
  var previousPenguin = this.element.querySelector('#previousPenguin');
  previousPenguin.addEventListener('click', this.onClickGetPenguin);

  var nextPenguin = this.element.querySelector('#nextPenguin');
  nextPenguin.addEventListener('click', this.onClickGetPenguin);
  nextPenguin.focus();
};

Note its main concern is to turn view model data into HTML and change the state. The second is to wire up click events and let the controller serve as the entry point. The event handlers get attached to the DOM after the state changes. This technique handles event management in one clean sweep.

To test this, we can verify the element gets updated and changes state:

var ElementMock = function ElementMock() {
  this.innerHTML = null;
};

// Stub functions, so we can pass the test
ElementMock.prototype.querySelector = function querySelector() { };
ElementMock.prototype.addEventListener = function addEventListener() { };
ElementMock.prototype.focus = function focus() { };

// Arrange
var elementMock = new ElementMock();

var view = new PenguinView(elementMock);

var viewModel = {
  name: 'Chinstrap',
  imageUrl: 'http://chinstrap1.jpg',
  size: '5.0kg (m), 4.8kg (f)',
  favoriteFood: 'krill',
  previousIndex: 1,
  nextIndex: 2
};

// Act
view.render(viewModel);

// Assert
assert(elementMock.innerHTML.indexOf(viewModel.name) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.imageUrl) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.size) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.favoriteFood) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.previousIndex) > 0);
assert(elementMock.innerHTML.indexOf(viewModel.nextIndex) > 0);

This about solves all the big concerns, changing state and wiring events. But, where is the data coming from?

The Model

In MVC, all the model cares about is Ajax. For example:

var PenguinModel = function PenguinModel(XMLHttpRequest) {
  this.XMLHttpRequest = XMLHttpRequest;
};

Note the module XMLHttpRequest gets injected into the constructor. This is a way to let fellow programmers know what components are necessary for this model. If the model needs more than simple Ajax, you can signal this with more modules. Plus, with unit tests, I can inject mocks that have the exact same contract as the original module.

Time to get a penguin based on an index:

PenguinModel.prototype.getPenguin = function getPenguin(index, fn) {
  var oReq = new this.XMLHttpRequest();

  oReq.onload = function onLoad(e) {
    var ajaxResponse = JSON.parse(e.currentTarget.responseText);
    // The index must be an integer type, else this fails
    var penguin = ajaxResponse[index];

    penguin.index = index;
    penguin.count = ajaxResponse.length;

    fn(penguin);
  };

  oReq.open('GET', 'https://codepen.io/beautifulcoder/pen/vmOOLr.js', true);
  oReq.send();
};

This points to an endpoint and gets the data from a server. We can test this by mocking the data with a unit test:

var LIST_OF_PENGUINS = '[{"name":"Emperor","imageUrl":"http://imageUrl",' +
  '"size":"36.7kg (m), 28.4kg (f)","favoriteFood":"fish and squid"}]';

var XMLHttpRequestMock = function XMLHttpRequestMock() {
  // The system under test must set this, else the test fails
  this.onload = null;
};

XMLHttpRequestMock.prototype.open = function open(method, url, async) {
  // Internal checks, system under test must have a method and url endpoint
  assert(method);
  assert(url);
  // If Ajax is not async, you’re doing it wrong :-)
  assert.strictEqual(async, true);
};

XMLHttpRequestMock.prototype.send = function send() {
  // Callback on this object simulates an Ajax request
  this.onload({ currentTarget: { responseText: LIST_OF_PENGUINS } });
};

// Arrange
var penguinModel = new PenguinModel(XMLHttpRequestMock);

// Act
penguinModel.getPenguin(0, function onPenguinData(penguinData) {

  // Assert
  assert.strictEqual(penguinData.name, 'Emperor');
  assert(penguinData.imageUrl);
  assert.strictEqual(penguinData.size, '36.7kg (m), 28.4kg (f)');
  assert.strictEqual(penguinData.favoriteFood, 'fish and squid');
  assert.strictEqual(penguinData.index, 0);
  assert.strictEqual(penguinData.count, 1);
});

As you can see, the model only cares about raw data. This means working with Ajax and JavaScript objects. If you are unclear about Ajax in vanilla JavaScript, there is an article with more info.

Unit tests

With any discipline, it is important to do the work necessary to get reassurance. The MVC design pattern does not dictate how you solve the problem. A design pattern gives you a broad set of boundaries that empower you to write clean code. This grants you freedom from dependency oppression.

To me, this means having a full suite of unit tests for each use case. The tests provide guidance on how the code is useful. This makes it open and inviting for any programmer looking to make specific changes.

Feel free to poke around the entire set of unit tests. I think it’ll help you understand this design pattern. Each test is for a specific use case; think of it as granular concerns. Unit tests help you think about each coding problem in isolation and solve for this one concern. This separation of functional concerns in MVC comes to life with each unit test.

Looking Ahead

The demo of penguins has only the bare viable concept to show how useful MVC can be. But, there are many improvements one can iterate on:

  • Add a screen with a list of all penguins
  • Add keyboard events so you can flip through penguins, add swipe too
  • An SVG chart to visualize data, pick any data point such as penguin size

Of course, it is up to you, my gentle reader, to take this demo further. These are but a few ideas so you can show how powerful this design pattern is.

Conclusion

I hope you see where the MVC design pattern and a little discipline can take you. A good design pattern stays out of the way while promoting clean code. It keeps you on task while solving the problem at hand only. It makes you a better more effective programmer.

In programming, the idea is to stay close to the problem at hand while eliminating cruft. The art of programming is fleshing out one single problem at a time. In MVC, this means one single functional concern at a time.

As a developer, it is easy to believe you are logical and don’t deal with emotion. The truth is as you get smacked around with too many problems at one time you get frustrated. This is a normal human response we all have to deal with. The fact is frustration affects code quality in a negative way. When this feeling grabs hold of you and dominates your work it is no longer about logic. As the solution takes on more risk and complex dependencies this can be demoralizing.

What I like is focusing on a single concern. Solving for one problem at a time and getting positive feedback. This way you stay in the zone, productive, and free from nonsense.

This article was peer reviewed by Vildan Softic. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!