Skip to content

Instantly share code, notes, and snippets.

@gschema
Last active November 27, 2023 04:35
Show Gist options
  • Save gschema/4157554 to your computer and use it in GitHub Desktop.
Save gschema/4157554 to your computer and use it in GitHub Desktop.
Basic JavaScript MVC Implementation

Basic JavaScript MVC Implementation

Despite being derived from classical MVC pattern JavaScript and the environment it runs in makes Javascript MVC implementation have its own twists. Lets see how typical web MVC functions and then dive into simple, concrete JavaScript MVC implementation.

How Web MVC typically works

Typical server-side MVC implementation has one MVC stack layered behind the singe point of entry. This single point of entry means that all HTTP requests, e.g. http://www.example.com or http://www.example.com/whichever-page/ etc., are routed, by a server configuration, through one point or, to be bold, one file, e.g. index.php.

At that point, there would be an implementation of Front Controller pattern which analyzes HTTP request (URI at first place) and based on it decides which class (Controller) and its method (Action) are to be invoked as a response to the request (method is name for function and member is name for a variable when part of the class/object).

Once invoked Controller takes over and passes to and/or fetches data from appropriate Model for the Action in concern. After Controller receives the data from Model it loads the view, which corresponds to the invoked Action, injects the data into it and returns the response to the user.

For example, let say we have our blog on www.example.com:

on sever side Front Controller would analyze URI and invoke Article Controller (corresponds to /article/ part of the URI) and its Edit Action (corresponds to /edit/ part of the URI). Within the Action there would be a call to, let say, Articles model and its Articles::getEntry(43) method (43 corresponds to /43/ in URI) which would return the blog article for edit. Afterwards, Article Controller would load (article/edit) view which would have logic for injecting the article's data into the form for user to edit its content, title and other (meta) data. Once all is done response is returned to the browser. As you can imagine, similar flow would be with POST request after we press save button in the loaded form. POST action URI would be like /article/save/43. This time request would go through the same Controller, but Save Action (due to /save/ URI chunk), and invoked Articles Model would save edit article to the database with Articles::saveEntry(43) and redirect back to the /article/edit/43 for further editing.

on sever side Front Controller would invoke default Controller and Action, e.g. Index Controller and its Index action. Within Index Action there would be a call to Articles model and its Articles::getLastEntries(10) method which would return last 10 blog posts. Afterwards, Controller would load blog/index view which would have basic logic for listing last 10 blog posts.

Let see big picture of typical HTTP request lifecycle through the server side MVC once again in the picture below. Server receives request and routes it through Front Controller. Front controller analyze request (URI) and invokes appropriate Action of the appropriate Controller. Within the Action a Model is asked to return and/or save submitted data. In the end, View is loaded by Controller and it executes presentation logic (loops through articles and prints title, content etc.) with provided data.


Simplified Web MVC Flow

MVC the JavaScript Way

Complex JavaScript web application, Single Page Application (SPA), dances all the time in a user's browser with all data persistence (saving to database on server) work done with Ajax calls in the background, which means, to put it boldly, no full browser refresh happens. Application behavior to be perceived by the users as dancing involves a lot of thought and work to be put in. Through evolution, trial and error, and a lot of spaghetti and not so spaghetti-like code developers in the end developed on ideas of traditional MVC paradigm and brought (big) part of the solution for structuring JavaScript code to the landscape of the SPAs through JavaScript MVC frameworks.

Typical page in a SPA consists of smaller ingredients which, when looked at deeper level, represent logical entities, which involve certain data domain that should be represented in a certain way on the page, e.g. a basket in an e-commerce web application which would typically have list of items, total price etc. and presented to the user as box in top right corner of the page (see the picture).

![wireframe](https://gist.githubusercontent.com/g6scheme/4157554/raw/b9791caa093572bf9d00891ab300e1ca964b1d0e/wireframe_e_commerce.png)
E-commerce wireframe representing modules within app which would have its own MVC stack

With this in mind it is obvious that each part, or a region or a widget, of the page in SPA needs separate set of MVC stack to take care of it. Derived from that, it means that typical SPA page consists of a set of MVC stacks with each MVC stack responsible for certain part of the page (for synchronizing its view - DOM, model and controller). That way code is better structured, decoupled and easier to maintain.

Still structuring those MVC stacks and organizing them to work seamlessly among each another can lead to problems down the road, as well, so it should be taken into account when designing application.

Let's see a simple implementation of the MVC and its usage in vanilla JavaScript, to clarify some concepts.

Event System Example

At heart of JavaScript MVC is Event system based on Publisher-Subscriber Pattern which makes possible for MVC components to intercommunicate in an elegant, decoupled manner. Event is inherited by View and Model component in a MVC implementation. That way each of them can inform other component that event of interest to them happened.

// Mix in to any object in order to provide it with custom events.
var Events = Cranium.Events = {
  channels: {},
  eventNumber: 0,
  // Used to publish to subscribers that an event of their interest happened
  trigger: function (events, data) {
    for (var topic in Cranium.Events.channels){
      if (Cranium.Events.channels.hasOwnProperty(topic)) {
        if (topic.split("-")[0] == events){
          Cranium.Events.channels[topic](data) !== false || delete Cranium.Events.channels[topic];
        }
      }
    }
  },
  // Used to register for the event to listen
  on: function (events, callback) {
    Cranium.Events.channels[events + --Cranium.Events.eventNumber] = callback;
  },
  // Used to unsubscribe/stop listening to the event
  off: function(topic) {
    delete Cranium.Events.channels[topic];
  }            
};

Event system makes possible:

  • for View to notify its subscribers of users interaction, like click or input in form etc., to update/re-render its UI etc..
  • for Model once its data are changed it can notify its listeners to update themselves (e.g. view to re-render to show accurate/updated data) etc­.

Model Example Implementation

Model takes care of data for managing data for the domain in concern. It does synchronization with server, notifying its subscribers of data change and other data related tasks.

// Domain-related data model
var Model = Cranium.Model = function (attributes) {
  this.id = _.uniqueId('model');
  this.attributes = attributes || {};    
};

Cranium.Model.prototype.get = function(attr) {
  return this.attributes[attr];
};
                 
Cranium.Model.prototype.set = function(attrs){
  if (_.isObject(attrs)) {
    _.extend(this.attributes, attrs);
    this.change(attrs);
  }
  return this;
};
            
Cranium.Model.prototype.toJSON = function(options) {
  return _.clone(this.attributes);
};

Cranium.Model.prototype.change = function(attrs){
  this.trigger(this.id + 'update', attrs);
}; 

_.extend(Cranium.Model.prototype, Cranium.Events);

View Example Implementation

View's main tasks are to render provided HTML template with correct data from the model.

// DOM View                                    
var View = Cranium.View = function (options) {
  _.extend(this, options); 
  this.id = _.uniqueId('view');
};

_.extend(Cranium.View.prototype, Cranium.Events);

Controller Example Implementation

Controller ties Model and View together. It listens to events (e.g. user form inputs) happening on its accociated View's DOM element (and its descendants) and notifies subscribers (e.g. Model, so it could update itself) about those events. Controller, also, instructs its View to update (re-render) itself to Model's current state on change events within the Model.

// Controller tying together a model and view
var Controller = Cranium.Controller = function(options){
  _.extend(this, options); 
  this.id = _.uniqueId('controller');
  var parts, selector, eventType;
  if(this.events){
    _.each(this.events, function(method, eventName){
      parts = eventName.split('.');
      selector = parts[0];
      eventType = parts[1];
      $(selector)['on' + eventType] = this[method];
    }.bind(this));
  }    
};

Usage

<div class="container">Foo</div>

<button id="inc">Increment</button>
<button id="alerter">Alert</button>

<script type="text/template" class="counter-template">
    <h1><%= counter %></h1>
</script>
// Let's create a basic application

var myModel = new Cranium.Model({
  counter: 0,
  incr: function () {
    myModel.set({ counter: ++this.counter });
  }
});

var myView = new Cranium.View({
  el: '.container',
  template: _.template($('.counter-template').innerHTML),
  
  observe: function (model) {
    this.on(model.id + 'update', function (data) {
      
     $(this.el).innerHTML = this.template( model.toJSON() );
      
    }.bind(this));
  }   
});

var myController = new Cranium.Controller({

  // Specify the model to update
  model: myModel,

  // and the view to observe this model
  view:  myView,
  
  events: {
    "#inc.click" : "increment",
    "#alerter.click" : "alerter"
  },

  // Initialize everything
  initialize: function () {
    this.view.observe(this.model);
    return this;
  },
  increment: function () {
    myController.model.attributes.incr();
    return this;
  },
  alerter: function(){
   alert("Yo!"); 
  }
});

// Let's kick start things off
myController.initialize(myModel, myView).increment().increment();

// Some further experiments with Underscore utils
var myModel2 = new Cranium.Model({
  caption: 'hello!'
});
            
console.log(_.any([myModel, myModel2, null]));
console.log(_.pluck([myModel, myModel2], 'id'));
console.log(_.shuffle([myModel, myModel2]));
@dmytro1
Copy link

dmytro1 commented May 31, 2018

Hey @gschema could you please explain where we declare "Cranium" object ?
I got 'ReferenceError: Cranium is not defined'
Thanks!

@gschema
Copy link
Author

gschema commented Jul 3, 2020

@dmytro1 @d-asensio @aissa-bouguern @hubisan

Hey guys,
I's completely unaware of comments on this gist.

It was ages ago so can't recall exactly. Afaik, I was using this gist while working out the way to simplify explanation of concepts to everyone (no matter the background - CS, SE or self-thought). It was part of my work while contributing to Addy Osmani's book Developing Backbone.js Applications:

https://addyosmani.com/backbone-fundamentals/

There are really good explanations in there on MVC and other concepts you (or someone you know) might be interested in and find helpful. Heaps of still relevant stuff there.

In any case, I am glad I've seen this and that it helped someone. 👍

@rafiramadhana
Copy link

@gschema Hi, thanks for the writing. Anyway, it looks like the link https://addyosmani.com/backbone-fundamentals/ is broken now.

Was it moved somewhere? Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment