Pure JavaScript MVC

Original author: Camilo Reyes
  • Transfer
Design patterns are often embedded in popular frameworks. For example, the MVC pattern (Model-View-Controller, Model-View-Controller) can be found literally everywhere. In JavaScript, it is difficult to separate the framework from the design pattern implemented in it, and often, framework authors interpret MVC in their own way and impose their vision on the question for programmers. How a particular MVC implementation will look exactly depends on the framework. As a result, we get a lot of different implementations, which is confusing and leads to disorder. This is especially noticeable when several frameworks are used in one project. This situation made me wonder: "Is there a better way?"





The MVC template is good for client frameworks, however, relying on something "modern", you need to remember that tomorrow something new will appear, and what is modern today will become obsolete. This is also a problem, and I would like to explore alternatives to frameworks and see where all this can lead.

The MVC design pattern appeared several decades ago. I believe that any programmer should invest time in his study. This template can be used without reference to any frameworks.

Is MVC implementation another framework?


To begin with, I would like to dispel one common myth when they put an equal sign between design patterns and frameworks. These are different things.

A template is a rigorous approach to solving a problem. To implement the template, for the details of which the programmer is responsible, a certain level of qualification is needed. The MVC pattern allows you to follow the principle of separation of responsibilities and helps write clean code.

Frameworks are not tied to specific design patterns. In order to distinguish a framework from a template, you can use the so-called Hollywood principle: “don’t call us, we will call you ourselves”. If the system has a certain dependence and at the same time in a certain situation you are forced to use it - this is the framework. The similarity of the frameworks with Hollywood lies in the fact that the developers who use them are similar to actors who are forced to strictly follow the scripts of films. Such programmers have no voting rights.

Should client frameworks be avoided? Everyone will answer this question, however, here are a few good reasons to refuse them:

  • Frameworks complicate decisions and increase the risk of failures.
  • The project is highly dependent on the framework, which leads to the emergence of difficult to maintain code .
  • When new versions of frameworks appear, it is difficult to rewrite existing code for them.

MVC Template


The MVC design pattern hails from the 1970s. He appeared at the Xerox PARC Research Center while working on the Smalltalk programming language. The template has passed the test of time in the development of graphical user interfaces. He came to web programming from desktop applications and proved his worth in a new field of application.

In essence, MVC is a way to clearly share responsibilities. As a result, the design of a solution based on it is clear even to a new programmer who for some reason joined the project. As a result, even those who were not familiar with the project can easily figure it out and, if necessary, contribute to its development.

MVC implementation example


To make it fun, we’ll write a web application that will focus on penguins. By the way, these cute creatures, like plush toys, live not only in the Antarctic ice. In total, there are about a dozen species of penguins. It's time to take a look at them. Namely, our application will consist of one web page on which there is an area for viewing penguin information and a couple of buttons that allow you to view the penguin catalog.

When creating the application, we will use the MVC template, strictly following its principles. In addition, the methodology of extreme programming , as well as unit tests, will be involved in the process of solving the problem . Everything will be done in JS, HTML and CSS - no frameworks, nothing more.

Having mastered this example, you will learn enough to integrate MVC into your own projects. In addition, since the software built using this template lends itself well to testing, along the way you will become familiar with the unit tests for our training project. Of course, if necessary, you can also take them into service.

We will adhere to the ES5 standard for cross-browser compatibility. We believe that the MVC template deserves to use well-known, proven features of the language for its implementation.

So let's get started.

Project Overview


The demonstration project will consist of three main components: the controller, presentation, and model. Each of them has its own sphere of responsibility and is focused on solving a specific problem.

This is how it looks like a diagram.


Project Diagram The

controllerPenguinController handles events and serves as an intermediary between the view and the model. He finds out what happened when the user performs an action (for example, clicks a button or presses a key on a keyboard). The logic of client applications can be implemented in the controller. On larger systems that need to handle many events, this element can be divided into several modules. The controller is the entry point for events and the only intermediary between the view and the data.

The viewPenguinView interacts with the DOM. The DOM is the browser API used to work with HTML. In MVC, only the view is responsible for DOM changes. A view can hook up UI event handlers, but event handling is the controller's prerogative. The main task solved by the presentation is to control what the user sees on the screen. In our project, the view will manipulate the DOM using JavaScript.

ModelPenguinModelresponsible for working with data. In client-side JS, this means performing Ajax operations. One of the advantages of the MVC template is that all interaction with the data source, for example, with the server, is concentrated in one place. This approach helps programmers who are not familiar with the project to figure it out. The model in this design pattern is exclusively concerned with working with JSON or objects that come from the server.

If, when implementing MVC, we violate the above separation of responsibilities between components, we get one of the possible anti-patterns of MVC. The model should not work with HTML. The view should not execute Ajax requests. The controller must play the role of an intermediary, not caring about the details of the implementation of other components.

If a web developer, when implementing MVC, does not pay enough attention to the separation of responsibilities of components, everything, in the end, turns into one web component. As a result, despite good intentions, it is a mess. This is due to the fact that increased attention is paid to the capabilities of the application and everything related to user interaction. However, the separation of responsibilities of components in the areas of program capabilities is not the same as the separation of functions.

I believe that in programming you should strive for a clear separation of the functional areas of responsibility. As a result, each task is provided with a uniform way to solve it. This makes the code clearer, allows new programmers connecting to the project to quickly understand the code and start productive work on it.

Perhaps quite a bit of reasoning, it's time to take a look at a working example, the code of which is posted on CodePen . You can experiment with it.


CodePen Application

Consider this code.

Controller


The controller interacts with the view and model. The components necessary for the operation of the controller are available in its constructor:

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

The designer uses inversion of control , the modules are embedded in it in accordance with this idea. Inversion of control allows you to implement any components that match certain high-level contracts. This can be considered as a convenient way to abstract from implementation details. This approach helps write clean JavaScript code.

Then the events related to user interaction are connected:

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 that event handlers use application state data stored in the DOM. In this case, this information is enough for us. The current state of the DOM is what the user sees in the browser. Application state data can be stored directly in the DOM, but the controller should not independently affect the state of the application.

When an event occurs, the controller reads the data and makes decisions about further actions. We are currently talking about a callback function this.showPenguin():

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, based on the state of the application and on the event that has occurred, finds the index corresponding to the penguin data set and informs the view of what exactly needs to be displayed on the page. The controller takes the necessary data from the model and converts it into an object with which the view can work.

The unit tests presented here are built on the AAA model (Arrange, Act, Assert - placement, action, approval). Here is a unit test for a standard penguin display scenario:

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 stub object PenguinViewMockimplements the same contract as the real view module. This allows you to write unit tests and check, in the Assert block, whether everything works as it should.

The object is asserttaken from Node.js , but you can use a similar object from the Chai library . This allows you to write tests that can be performed both on the server and in the browser.

Note that the controller does not care about implementation details. He relies on a contract that provides a view, sort ofthis.render(). This is exactly the approach you need to follow to write clean code. The controller, with this approach, can entrust the component with the execution of the tasks that the component has announced the possibility of performing it. This makes the project structure transparent, which improves code readability.

Representation


The view only cares about the DOM elements and the connection of event handlers. For instance:

var PenguinView = function PenguinView(element) {
  this.element = element;
  this.onClickGetPenguin = null;
};

Here's how the effect of the view on what the user sees is implemented in the code:

PenguinView.prototype.render = function render(viewModel) {
  this.element.innerHTML = '

' + viewModel.name + '

' +    '' + viewModel.name + '' +    '

Size: ' + viewModel.size + '

' +    '

Favorite food: ' + viewModel.favoriteFood + '

' +    ' ' +    '';  this.previousIndex = viewModel.previousIndex;  this.nextIndex = viewModel.nextIndex;  // Подключение обработчиков событий щелчков по кнопкам и передача задачи обработки событий контроллеру  var previousPenguin = this.element.querySelector('#previousPenguin');  previousPenguin.addEventListener('click', this.onClickGetPenguin);  var nextPenguin = this.element.querySelector('#nextPenguin');  nextPenguin.addEventListener('click', this.onClickGetPenguin);  nextPenguin.focus(); }

Please note that the main task of the view is to convert the data received from the model into HTML and change the state of the application. Another task is to connect event handlers and transfer their processing functions to the controller. Event handlers connect to the DOM after a state change. This approach allows you to easily and conveniently manage events.

In order to test all this, we can check the update of elements and the change in the state of the application:

var ElementMock = function ElementMock() {
  this.innerHTML = null;
};
// Функции-заглушки, необходимые для того, чтобы провести тестирование
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);

We reviewed the view and controller. They solve most of the tasks assigned to the application. Namely, they are responsible for what the user sees and allow him to interact with the program through the event mechanism. It remains to understand where the data comes from, which are displayed on the screen.

Model


In the MVC template, the model is busy interacting with the data source. In our case, with the north. For instance:

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

Please note that the module is XMLHttpRequestimplemented in the model constructor. This, among other things, is a hint to other programmers regarding the components needed by the model. If the model needs various ways of working with data, other modules can be embedded in it. As in the cases considered above, unit tests can be prepared for the model.

Get the penguin data based on the 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);
    // Индекс должен быть целым числом, иначе это работать не будет
    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();
};

Here you connect to the server and download data from it. Verify the component using a unit test and conditional test data:

var LIST_OF_PENGUINS = '[{"name":"Emperor","imageUrl":"http://imageUrl",' +
  '"size":"36.7kg (m), 28.4kg (f)","favoriteFood":"fish and squid"}]';
var XMLHttpRequestMock = function XMLHttpRequestMock() {
  // Для целей тестирования нужно это установить, иначе тест не удастся
  this.onload = null;
};
XMLHttpRequestMock.prototype.open = function open(method, url, async) {
  // Внутренние проверки, система должна иметь конечные точки method и url
  assert(method);
  assert(url);
  // Если Ajax не асинхронен, значит наша реализация весьма неудачна :-)
  assert.strictEqual(async, true);
};
XMLHttpRequestMock.prototype.send = function send() {
  // Функция обратного вызова симулирует Ajax-запрос
  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 cares only for the raw data. This means working with Ajax and with JavaScript objects. If you don’t quite own the Ajax theme in JavaScript, here is some helpful material on this.

Unit tests


With any rules for writing code, it is important to check what happened. The MVC design pattern does not govern how to solve the problem. Within the framework of the template, borders are quite free, without crossing which you can write clean code. This gives freedom from the dominance of addictions.

I usually strive to have a complete set of unit tests covering every use case of a product. These tests can be considered recommendations for the use of code. This approach makes the project more open, understandable for any programmer who wants to participate in its development.

Experiment with a complete setunit tests. They will help you better understand the design pattern described here. Each test is designed to test a specific use case of the program. Unit tests will help to clearly separate tasks, and, when implementing a project’s functionality, not be distracted by other parts of it.

About the development of the educational project


Our project implements only the basic functionality necessary to demonstrate the implementation of MVC. However, it can be expanded. For example, here are some of the possible improvements:

  • Add a screen listing all penguins.
  • Add keyboard event processing to organize an alternative way to switch between penguin cards. Similarly, for mobile devices, you can add gesture control on the touch screen.
  • Add SVG diagram for data visualization. For example, this way you can display generalized information about the size of penguins.

If you want to develop this project as an exercise, consider that only a few ideas are given above. You may well, using MVC, implement other functionality.

Summary


We hope you appreciate the benefits of the MVC design pattern and the disciplined approach to the architecture and project code. A good design pattern, on the one hand, does not interfere with work, on the other hand, it helps to write clean code. He, in the process of solving the problem, does not allow you to turn off the right path. This increases the efficiency of the programmer.

Programming is the art of sequential problem solving, dividing the system into small parts that implement a certain functionality. In MVC, this means strict adherence to the principle of separation of the functional responsibilities of components.

Developers usually believe that they are not susceptible to the action of emotions, that their actions are always logical. However, when trying to simultaneously solve several problems, a person finds himself in a difficult psychological situation. The load is too high. Work in this state has a bad effect on the quality of the code. At a certain moment, logic fades into the background, as a result, the risk of errors increases, the quality of the code deteriorates.

That is why it is recommended to break up projects into small tasks and solve them in turn. A disciplined approach to MVC implementation and preparation of unit tests for various parts of the system contribute to a calm and productive work.

Dear readers! What design patterns do you use in your JS projects?

Also popular now: