Hackernoon logoHow I Adopted the Model, View, Controller (MVC) Architectural Pattern in JavaScript by@Aaron Rory

How I Adopted the Model, View, Controller (MVC) Architectural Pattern in JavaScript

Author profile picture

@Aaron RoryAaron Newbold

Full-Stack Developer - JavaScript, React, Ruby, Rails. Portfolio: https://aaronrory.com

...for Better Code Separation

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

            Sources: Rails Documentation
The MVC architecture divides your code into three (3) layers Models, Views and Controllers separating the different responsibilities of the program.
Image from Wikipedia
Model Layer
This layer in Ruby on Rails contains the domain model, which generally represents a specific object class (e.g. a Person, Animal, Books). This is where the business logic is usually handled since this model is linked to the database and its data is derived from the relative table’s rows.
View Layer
Handles the visual representations of the responses provided by the controllers. Since the controller can return information as HTML, XML, JSON, etc.
Controller Layer
In Rails this layer is responsible for interacting with the model, manipulating its data and providing suitable responses to various HTTP requests.

Now, what would this MVC pattern look like in JavaScript?

Source: MDN web docs
Since JavaScript generally does not involve the use of databases (although it could), nor HTTP request handling (Once again it could), the MVC pattern will have to be tweaked a bit to fit the language specificity.
Image from MDN

Model Layer

The model layer can literally be as simple as an array but usually it would be a class. An application can have multiple models and these classes (models) would contain the basic data needed for the app to function.
E.G. Take for instance a Classroom App which tracks which classes a person is taking. The models in this case can be divided into classes such as a 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;
  }
}
The Classroom model contains data variables which will hold information for each classroom. This would be a list of all the persons currently enrolled in this classroom, the subject associated with this classroom and it's ID.
class Person {
  constructor(id, firstN = 'John', lastN = 'Doe') {
    this.id = id;
    this.firstName = firstN;
    this.lastName = lastN;
    this.subjects = [];
    this.classrooms = [];
  }
}
The Person model contains data variables which will hold information for each person. This would be their first and last name, the subjects that they are studying and which classrooms they are apart of.
const subjects = [
  "English",
  "Math",
  "Computer Science",
  "Business",
  "Finance",
  "Home Economics"
];
The Subjects model will simply be an array, since for this example I have no intention in allowing the subjects model to be manipulated.

Controller Layer

The controller would be a class which translates the user input into changes to the model's data.
E.G. In the Classroom App – the controller receives user input from view elements such as text input or select options and button clicks which is used to modify 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);
  }
}
The classroom controller in this case could be seen as a Table in reference to how Rails operates and each row in this "Table" would be information linked to each classroom object already created.
This controller has three variables of its own, "lastID" (Every time a classroom object is created and added to the classrooms array this variable's value increments), "classrooms" (an array of all created classroom objects) and "selectedClass".

View Layer

This layer handles the visual representation of the app’s data. This layer contains classes which allows the User to see and interact with the data.
E.G. In the Classroom App – the view would provide Document Object Model (DOM) elements such as Buttons, Inputs and containers (<div/>, <span/ >, <p/>…etc.) to display the various persons and classrooms and their related 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();
  }
}
The ClassroomView class contains a variable that links to a ClassroomController which is created upon construction. This allows communication with the controller from the view.
The updateView() function is ran after every change as a result from user Interactions. This function simply updates all related view DOM elements with the appropriate data obtained from the associated model.
All functions within the view simply grab values from the User Interface (UI) DOM elements and transfer them as variables to the controller's functions. The functions selectClassroom(), addClassroom() and removeClassroom() are all added to the DOM elements via the updateView() function as events through the addEventListener() function.

Accessing all controllers and views through a single view

Now, since for this example we have two controllers, a Classroom Controller and a Person Controller (Shown in full project). We would also have two views and if we wanted these two views to be able to interact with one another we would create a single overarching view. We could call this view the Application View.
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();
  }
}
This ApplicationView class would have it's own variables that would link to both the classroom and person views. Since it has access to those two views it also has access to their controllers.
This button above is generated by the Application view. It grabs the selectedClassroom and selectedPerson values from their respective controllers and runs the addPerson() function in the classroom view upon interaction.

Some Advantages of Using the MVC Framework

1. Separation of Concerns
All code related to the User Interface is handled by the view. All base data variables are contained by the Model and all model data is changed through the Controller.
2. Simultaneous Development
Since this MVC model clearly separates the project into three (3) layers it becomes a lot easier to separate and assign tasks to multiple developers.
3. Ease of Modification
Can easily make changes to each layer without immediately affecting the other layers.
4. Test Driven Development (TDD)
With clear separation of concerns it allows the testing of each individual component independently.

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!