How I Implemented MVC in JavaScript

… for better code separability

For prospective students course “Architecture and design patterns” and all those interested have prepared a translation of useful material.

We also invite you to visit open webinar on the “Interpreter” topic. It will discuss the purpose and structure of the “Interpreter” pattern, Backus-Nauer forms, lexical, syntactic and semantic analysis.


What is the Model, View, Controller (MVC) architectural pattern?

A source: Rails documentation

The MVC architecture divides your code into three (3) layers: Models, Views, and Controllers, which perform various tasks within a program.

Image taken from Wikipedia

Model level

In Ruby on Rails, this level contains the domain model, which usually represents a specific class of objects (for example, Human, Animal, Books). This is usually where the business logic is handled because the model is linked to the database and the data for it is retrieved from the rows of the corresponding table.

Presentation layer

Processes a visual representation of the responses provided by controllers. Since the controller can return information in HTML, XML, JSON, etc.

Controller level

In Rails, this layer is responsible for interacting with the model, manipulating its data, and providing appropriate responses to various HTTP requests.

What would the MVC pattern look like in JavaScript?

A source: MDN documentation

Since JavaScript usually doesn’t involve using databases (although it might) or handling HTTP requests (again, it might), the MVC pattern will need to be tweaked a bit to suit the specific language.

Image via MDN

Model level

Even something as simple as an array can serve as the model level, but often it will be some kind of class. An application can have multiple models, and these classes (models) will contain the basic data required for the application to work.

Take, for example, the Classroom app, which keeps track of which classes a person is attending. In this case, the model level can be divided into classes such as Classroom, Person and an array based model called Subjects

Base Model Classes

class Classroom {
  constructor(id, subject="Homeroom") {
    this.id = id;
    this.persons = [];
    this.subject = subject;
  }
}

Model Classroom contains data variables that will contain information for each class. This will include a list of all people currently enrolled in that class, the subject associated with that class, and its id

class Person {
  constructor(id, firstN = 'John', lastN = 'Doe') {
    this.id = id;
    this.firstName = firstN;
    this.lastName = lastN;
    this.subjects = [];
    this.classrooms = [];
  }
}

Model Person contains variable data that will contain information about each person. This will include his first and last name, the subjects he is studying and the classes he attends.

const subjects = [
  "English",
  "Math",
  "Computer Science",
  "Business",
  "Finance",
  "Home Economics"
];

Model Subjects will just be an array, since for this example I’m not going to allow manipulation of the discipline model.

Controller level

The controller will be a class that translates user input into model data changes.

For example, in a Classroom app – the controller receives user input from view elements such as text input (text input) or selection from a list of options (select options), as well as the button presses used to change the model.

import classroomModel from "../models/classroom";

class ClassroomController {
  constructor() {
    this.lastID = 0;
    this.classrooms = [];
    this.selectedClass = null;
  }

  selectClassroom(classroomID) {
    this.selectedClass = this.classrooms
    .filter(c => c.id === parseInt(classroomID, 10))[0];
  }

  addClassroom(subject) {
    this.classrooms.push(
      new classroomModel(this.lastID, subject)
      );
    this.lastID += 1;
  }

  removeClassroom(classroomID) {
    this.classrooms = this.classrooms
      .filter(c => c.id !== parseInt(classroomID, 10));
  }

  setSubject(subject, classroomID) {
    const classroom = this.classrooms
      .filter(c => c.id === parseInt(classroomID, 10))[0];
    classroom.subject = subject;
  }

  addPerson(person, classroom) {
    // const classroom = this.classrooms
    // .filter(c => c.id === parseInt(classroomID, 10))[0];
    if (!person) return;
    classroom.addPerson(person);
  }

  removePerson(person, classroomID) {
    const classroom = this.classrooms
    .filter(c => c.id === parseInt(classroomID, 10))[0];
    classroom.removePerson(person);
  }
}

In this case ClassroomController can be thought of as a table (compared to how Rails works), and each row in that “table” will represent information associated with each class object already created.

This controller has three own variables: “lastID“(Each time a class object is created and added to the array of classes, the value of this variable is incremented),”classrooms“(An array of all created objects of the class) and”selectedClass“.

Presentation layer

This layer processes the visual presentation of application data. This level contains classes that allow the user to see and interact with data.

For example, in a Classroom app – the view will provide elements DOM (document object model), such as buttons, inputs and containers (

, ,

… etc.) to display different people and classes, and their associated data.
import classroomController from "../controllers/classroom";
import subjects from "../models/subjects";

class ClassroomView {
  constructor(appDiv) {
    this.classroomController = new classroomController();
    this.classroomSectionDiv = document.createElement('div');
    this.classroomsDiv = document.createElement('div');
    this.addclassBtn = document.createElement('button');
    this.selectSubjectInput = document.createElement('select');

    this.classroomSectionDiv.classList.add('classroom-section');
    this.classroomsDiv.classList.add('classroom-container');
    this.selectSubjectInput.innerHTML = subjects.map((option, index) => (
      `<option key=${index} value=${option}>${option.toUpperCase()}</option>`
    ));
    this.addclassBtn.textContent="New Class";
    this.addclassBtn.addEventListener('click', () => this.addClassroom());
    this.classroomSectionDiv.append(
      this.classroomsDiv, this.selectSubjectInput,
      this.addclassBtn,
      );
    appDiv.appendChild(this.classroomSectionDiv);
  }

  updateView() {
    const { classroomController, classroomsDiv } = this;
    const allClassrooms = classroomController.classrooms.map(
      c => {
        const removeBtn = document.createElement('button');
        const classDiv = document.createElement('div');
        classDiv.classList.add('classroom');
        if (classroomController.selectedClass === c) {
          classDiv.classList.add('selected');
        }
        classDiv.addEventListener('click', () => this.selectClassroom(classDiv.getAttribute('data-classroom-id')));
        classDiv.setAttribute('data-classroom-id', c.id);
        removeBtn.addEventListener('click', () => this.removeClassroom(removeBtn.getAttribute('data-classroom-id')));
        removeBtn.setAttribute('data-classroom-id', c.id);
        removeBtn.classList.add('remove-btn');
        removeBtn.textContent="remove";
        const allPersons = c.persons.map(p => (
          `<div class="person-inline">
            <span class="fname">${p.firstName}</span>
            <span class="lname">${p.lastName}</span>
            <span class="${p.occupation}">${p.occupation}</span>
          </div>`
        ));
        classDiv.innerHTML = `<div class="m-b">
            <span class="id">${c.id}</span>
            <span class="subject">${c.subject}</span></div>
            <div class="all-persons">${allPersons.join('')}</div>`;
        classDiv.appendChild(removeBtn);
        return classDiv;
      }
    );
    classroomsDiv.innerHTML='';
    allClassrooms.map(div => classroomsDiv.append(div));
  }
  
  selectClassroom(classroomID) {
    const { classroomController } = this;
    classroomController.selectClassroom(classroomID); 
    this.updateView();
  }

  addClassroom() {
    const {
      classroomController,
      selectSubjectInput,
    } = this;
    const subjectChosen = selectSubjectInput.value;
    classroomController.addClassroom(subjectChosen);
    this.updateView();
  }

  removeClassroom(classroomID) {
    const { classroomController } = this;
    classroomController.removeClassroom(classroomID);
    this.updateView();
  }

  addPerson(person, classroomID) {
    const { classroomController } = this;
    classroomController.addPerson(person, classroomID);
    this.updateView();
  }
}

Class ClassroomView contains a variable that is associated with ClassroomControllerthat is created during construction. This allows the view tier to communicate with the controller.

Function updateView() runs after every change resulting from user interaction. This function simply updates any required DOM elements in the view with the appropriate data from the associated model.

All functions in the view simply grab values ​​from the UI elements of the DOM and pass them as variables to the controller functions. Functions selectClassroom(), addClassroom() and removeClassroom() are added to DOM elements via a function updateView() as events via function addEventListener()

Access all controllers and views with one view

Now, since we have two controllers for this example, ClassroomController and PersonController (can be found in the complete project), we would also have two views, and if we wanted these two views to interact with each other, we would have to create a single overarching view. We could call this performance AppView

import classroomView from './classroom';
import personView from './person';

class AppView {
  constructor(appDiv) {
    this.classroomView = new classroomView(appDiv);
    this.personView = new personView(appDiv);
    this.addPersonToClassBtn = document.createElement('button');

    this.addPersonToClassBtn.textContent="Add selected Person to Selected Class";
    this.addPersonToClassBtn.addEventListener('click', () => this.addPersonToClass());
    appDiv.appendChild(this.addPersonToClassBtn);
  }

  addPersonToClass() {
    const { classroomView, personView } = this;
    const { classroomController } = classroomView;
    const { personController } = personView;
    const selectedClassroom = classroomController.selectedClass;
    const selectedPerson = personController.selectedPerson;
    classroomView.addPerson(selectedPerson, selectedClassroom);
    personView.updateView();
  }
}

Class AppView will have its own variables that will bind to both ClassroomViewand with PersonView… Since it has access to these two views, it also has access to their controllers.

The button above is created AppView… It gets values selectedClassroom and selectedPerson from the respective controllers and, upon interaction, runs the function addPerson() in ClassroomView

For a full view of the Classroom app, go to CodeSandBox at this link

Some benefits of using MVC framework

Sources: Brainvire, c-sharpcorner, StackOverflow, Wikipedia

1. Separation of duties

All the code associated with the user interface is handled by the view. All the variables of the underlying data are contained in the model, and all the data in the model is modified using the controller.

2. Simultaneous development

Since the MVC model clearly divides the project into three (3) tiers, it becomes much easier to divide and distribute tasks among multiple developers.

3. Ease of modification

You can easily make changes at each level without affecting the rest of the levels.

4. Test Driven Development (TDD)

Thanks to a clear separation of duties, we can test each individual component independently.


Learn more about the course “Architecture and Design Patterns”.

Register now for an open webinar on the “Interpreter” topic.

Right now, OTUS has the maximum New Year discounts for all courses. You can see the full list of courses at the link below. Also, everyone has a unique opportunity to send to the addressee gift certificate for training at OTUS

By the way, about the “beautiful packaging” of online certificates, we we tell in this article

GET A DISCOUNT

Similar Posts

Leave a Reply Cancel reply