Implement MVC and PubSub in JavaScript

We know what is MVC? MVC stands for Model-View-Controller. In simple words, MVC is a design technique in which application components are divided into 3 groups so that they can be developed separately without concerning, how they will interact. If build properly, few configuration code will bind them and they will be ready to use.

PubSub (Publisher Subscriber) model is design paradigm where multiple subscribers are listening for change events on a source, and as soon as any change happen, listeners are notified. This pattern removes a lot of hard coding and provide design flexibility in large systems where user interaction affects multiple sections on a screen.

PubSub + MVC in JavaScript
PubSub + MVC in JavaScript

In this tutorial, we will learn following concepts:

Building Model-View-Controller components
Building Publisher Subscriber infrastructure
Understanding Event Notification mechanism
Demo application

Lets start with building MVC components.

Building Model-View-Controller components

In JavaScript, if we have to develop a MVC structure, we will need to write at least 3 objects. I am taking only 3 to make example more focused on concept.

For example purpose, I am taking a case of media player. This media player has one playlist attached and user can move forward and previous on this playlist using key press events.

Model: To store the current state of view

playlist – array object store all tracks in playlist currently available.
currentIndex – The currently playing track

Model also contains functions which help it to maintain its current state changes after user interaction.

var Model = {
   playlist: new Array(),
   currentIndex : 0,
   reLoad: function() {
		currentIndex = 0;
		var tracks = document.getElementById("playListSelector").options;
		for(var i=0; i<tracks.length; i++)
		{
			this.playlist&#91;i&#93; = tracks&#91;i&#93;.value;
		}
   },
   next: function () {
        if(this.currentIndex < (this.playlist.length-1))
			this.currentIndex++;
		publish(this);
   },
   prev: function () {
		if(this.currentIndex > 0)
			this.currentIndex--;
		publish(this);
   },
   current: function () {
		publish(this);
   }
};

View: To represent the screen with which user interact

This object has only one method which renders the result of user events on screen.

var View = {
   notify: function(model) {
		document.getElementById("playListSelector").selectedIndex = model.currentIndex;
   }
};

Controller: View invokes controller to change the model

Controller has functions which will be invoked during user interaction.

var Controller = {
   model: Model,
   moveNext: function () {
     this.model.next(); 
     return this; 
   },
   movePrev: function () {
     this.model.prev(); 
     return this; 
   },
   getCurrent: function () {
     this.model.current(); 
     return this; 
   }
};

Building Publisher Subscriber infrastructure

So far so good. Now we will add some pub-sub logic so that whenever any user event is triggered, all registered views get notified and they can make required visual changes.

//All subscribers for a event
var subscribers = [];

function publish(event) {
   for (i in subscribers) {
     subscribers[i].notify(event);
   }
};

Above code declares an array which can be used to store all interested views to registered themselves as event listeners. Whenever any event is triggered as user interaction, they will be notified of the event.

To register the views as event listener, following code will be used:

//Subscribe for updates
subscribers.push(View); 

Understanding Event Notification mechanism

The event handling is performed in following sequence:

View trigger the event -> Controller triggers the model update -> Model send the notifications to pubsub -> pubsub notify all views about event so that they can update the user screen

In above code snippets, let’s say user press next track on playlist. This is the flow of control:

  1. User presses button “Next Track”
  2. Controller’s moveNext() method is called
  3. moveNext() triggers model’s next() method
  4. next() method increment the currentIndex which is currently playing track
  5. next() method publish the event using publish() method
  6. publish() method invokes notify() method is all registered subscribers
  7. Views notify() method update the user screen based on current state of model

In this way, all possible events are handled from Controller to view layer. And at last, we have current state of model all the time.

Demo application

I have used all above code snippets in one file and made a virtual playlist behavior using HTML select element. Currently selected option of select represents the currently playing track in media player.

Lets have look at complete demo code:

<html>
<head>
<meta charset="utf-8">
<script language="javascript">
// PubSub
var subscribers = [];
function publish(event) {
   for (i in subscribers) {
     subscribers[i].notify(event);
   }
};

// MVC
var Model = {
   playlist: new Array(),
   currentIndex : 0,
   reLoad: function() {
		currentIndex = 0;
		var tracks = document.getElementById("playListSelector").options;
		for(var i=0; i<tracks.length; i++)
		{
			this.playlist&#91;i&#93; = tracks&#91;i&#93;.value;
		}
   },
   next: function () {
        if(this.currentIndex < (this.playlist.length-1))
			this.currentIndex++;
		publish(this);
   },
   prev: function () {
		if(this.currentIndex > 0)
			this.currentIndex--;
		publish(this);
   },
   current: function () {
		publish(this);
   }
};

var View = {
   notify: function(model) {
		document.getElementById("output").innerHTML = JSON.stringify(model);
		document.getElementById("playListSelector").selectedIndex = model.currentIndex;
   }
};


var Controller = {
   model: Model,
   moveNext: function () {
     this.model.next(); 
     return this; 
   },
   movePrev: function () {
     this.model.prev(); 
     return this; 
   },
   getCurrent: function () {
     this.model.current(); 
     return this; 
   }
};

subscribers.push(View); // Subscribe for updates

function initializeModel()
{
	Model.reLoad();
}

</script>
</head>
<body onload="initializeModel()">
	<input type="button" onclick="Controller.getCurrent();" value="Current Track">
	<input type="button" onclick="Controller.moveNext();" value="Next Track">
	<input type="button" onclick="Controller.movePrev();" value="Previous Track">
	
	<select id="playListSelector" multiple readonly>
		<option value="0">Track 1</option>
		<option value="1">Track 2</option>
		<option value="2">Track 3</option>
		<option value="3">Track 4</option>
	</select>
       <span id="output" />
</body>
</html>

Above code has one additional method initializeModel(), which is used to initialize the model object with playlist items on page load. Now when we press “Next track”, next option in select element is selected. Similarly, for pressing “Previous Track” button, previous option is selected in select list.

You will see the running code like this:

Demo Screen of MVC + PubSub w2ith JavaScript
Demo Screen of MVC + PubSub w2ith JavaScript

Please drop a comment if something is unclear or you have any suggestion/query.

———————————————————————————————————-
Update:

After a short discussion over mail, Brook Monroe sent me the better code example for similar example. Though the intent of this tutorial is not better code practices, rather detailing the concept. I am sharing the updated code below for reference. It might help you in need.

<html>
<head>
<meta charset="utf-8">
<script src="./pubsub.js"></script>
</head>
<body>
    <button id="btnCurrent">Current Track</button>
    <button id="btnNext">Next Track</button>
    <button id="btnPrev">Previous Track</button>
     
    <select id="playListSelector" multiple readonly>
        <option value="0" selected>Track 1</option>
        <option value="1">Track 2</option>
        <option value="2">Track 3</option>
        <option value="3">Track 4</option>
    </select>
    <span id="output"></span>
</body>
</html>

//pubsub.js


// PubSub
( function () {
  "use strict";
  var subscribers = [],
      elCache = {},
      Model = {
          playlist : [],
          currentIndex : 0,
          reLoad : function() 
          {
              var tracks = Array.prototype.slice.call(elCache.get("playListSelector").options);
              this.playlist = [];
              tracks.forEach( function (e,i) { this.playlist.push(tracks[i].value); }, Model);
              this.currentIndex = 0;
          },
          next : function () 
          {
              if (this.currentIndex < (this.playlist.length-1)) {
                this.currentIndex++;
              }
              subscribers.publish(this);
          },
          prev : function () 
          {
              if (this.currentIndex > 0) {
                  this.currentIndex--;
              }
              subscribers.publish(this);
          },
          current : function () 
          {
              subscribers.publish(this);
          }
      },
	// MVC
      View = {
        notify : function(model) 
        {
          elCache.get("output").innerHTML = JSON.stringify(model);
          elCache.get("playListSelector").selectedIndex = model.currentIndex;
        }
      },
      Controller = {
        moveNext: function () 
        {
          Model.next(); 
          return this; 
        },
        movePrev: function () 
        {
          Model.prev(); 
          return this; 
        },
        getCurrent: function () 
        {
          Model.current(); 
          return this; 
        }
      };

  function start()
  {
    elCache.get = function (elId)
    {
      return this[elId] || ( this[elId] = document.getElementById(elId) );
    };

    subscribers.publish = function (event)
    {
      this.forEach( function (e) { e.notify(event); } );
    };

    subscribers.push(View); // Subscribe for updates

    elCache.get("btnCurrent").addEventListener("click", Controller.getCurrent.bind(Model));
    elCache.get("btnNext").addEventListener("click", Controller.moveNext.bind(Model));
    elCache.get("btnPrev").addEventListener("click", Controller.movePrev.bind(Model));
    Model.reLoad.bind(Model)();
  }

  window.addEventListener("load",start,false);
} )();
 

Happy Learning !!

2 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments

Comments are closed for this article!

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.