Rediscovering MVC and How to Write without a Framework

If you’ve paid much attention to front-end development in the last few years you’ve heard about Angular, Backbone, Ember, and other JavaScript MV* frameworks. They offer structure, bundled APIs and streamlined approaches to complex UIs, although not without concerns of performance, monolithic designs, and high churn rates[1][6].

All these frameworks share an adaptation of classic MVC, a pattern that transcends platforms, libraries, and languages, where models hold state and logic, views visualize state as output, and controllers handle input and interactions with the model and view[2]. Understanding classic MVC helps us evaluate strengths and shortcomings when selecting a front-end framework or micro-framework alternative. In this post we’ll cover MVC’s relevance, the roles of each component and how to write it with vanilla JavaScript.


 Purpose

So why is MVC so prevalent? Some main advantages are:

  1. Separates presentation logic from application logic. Presentation relates to the UI, such as what <div>s, <form> controls or ajax spinners are shown at any point in time. Application or business logic includes data modeling, integrity, calculations, and so forth. This division is important as UIs are often device dependent and change more rapidly than application logic, preventing the need to retest models.
  2. Decouples event-based inputs from display outputs. While the views and controllers often have a close coupling, breaking I/O into two areas leads to more reusable parts where a given view can be directed by a selected controller based on the desired interaction. Controllers could change their method of interaction dynamically at runtime depending on the state of the model[4].
  3. Reuses data modeling across multiple views. M-to-1 view-to-model ratios allows a) the same data to be displayed in different ways such as a pie chart and bar chart pulling the same data, and b) multiple views of the same data shown at the same time. If the user changes data in one view, the change is propagated to the other views.
  4. Controls interactions between modules where data changes can propagate from model to model and presentation changes from controller to controller, avoiding reliance on global state.
  5. Can facilitate parallel development of UI and application based components on larger projects with multiple developers[2].


 Roles

One of the early and most cited MVC diagrams has the following references.

mvc29.png

[2]

Solid lines are dependencies and dashed lines are model’s observer pattern notifications (more below). It’s worth noting the earliest version has M-to-M ratios between view-to-controller and view-to-model[5].

 Models

Models handle application state from simple integers to a complex data structures[2].

Models have no references to views or controllers. Loose coupling is a goal of long term architecture as reflected by Addy Osmani.

We want a loosely coupled architecture with functionality broken down into independent modules with ideally no inter-module dependencies. Modules speak to the rest of the application when something interesting happens and an intermediate layer interprets and reacts to these messages.

[3]

 Views

Views handle output.  They are objects and not markup. They

In some interpretations of MVC the view references the controller but early descriptions don’t show this connection and the tighter coupling can be avoided using the observer pattern[4].

 Controllers

In classic MVC controllers handle all user input. In handlers they direct the view to do any presentation logic and update the model with any application data[2][4].

 Data Flow

Event based tasks differ, of course, case by case. Here’s a typical interaction cycle.

mvc15.png

  1. User triggers event (e.g. click, blur, hover) that’s handled by the controller
  2. Controller directs view to do some presentation logic (e.g. show spinner)
  3. Controller passes the new value to model
  4. Model updates it’s state with new value, saves to DB if needed, then notifies observers it has changed.
  5. Views query the model about relevant data
  6. Views build a data object, performing any presentation logic, and binds it to the template.


 Code

We can easily write MVC using vanilla JavaScript with a templating library (e.g. Handlebars) and jQuery for DOM API.

Let’s look at a basic implementation of ToDo MVC.

todo2.png

index.html

<!DOCTYPE>
<html>
<body>
    <!-- template -->
    <script id="todo-template" type="text/x-handlebars-template">
        {{#each view_list}}
            <li class="todo-item">
                <input class="input" type="checkbox" {{checked}}>
                <div class="{{checked}}">{{val}}</div>
                <div class="remove">X</div>
            </li>
        {{/each}}
    </script>

    <input id="todo-input"/>
    <ul id="todo-list">
        <!-- template inserted -->
    </ul>

    <script src="jquery.1.11.3.js"></script>
    <script src="handlebars-v4.0.5.js"></script>
    <script src="mvc.js"></script>
</body>
</html>

In mvc.js we have:

Model

/**
*  Model holds data with access and modify methods.  
*  register() adds items to subject.  When model state 
*  changes calls subject.notifyObservers() to redraw list.
*/
function ToDoModel() {
    const subject = Subject(),
        list = [];
    return {
        getList: function() {
            return list;
        },
        add: function (text) {
            list.push({ 
                val: text, 
                complete: false
            });
            subject.notifyObservers();
        },
        remove: function(index) {
            list.splice(index, 1);
            subject.notifyObservers();
        },
        complete: function(index, isComplete) {
            isComplete === true ?
                list[index].complete = true :
                list[index].complete = false;
            subject.notifyObservers();
        },
        // observer
        register: function(...args) {
            subject.removeAll();
            args.forEach(elem => {
                subject.add(elem);
            });
        }
    };
}

View

/**
*  View handle output to template.  On init gets DOM refs,
*  and expose to controller.  When model calls notify(), 
*  View queries model for data and data performs pres. logic.
*/
function ToDoView(model) {
    const DOM = {
        input: $('#todo-input'),
        list: $('#todo-list')
    },
    templateFnc = 
             Handlebars.compile($('#todo-template').html());

    function getData() {
         // presentation logic
         function isChecked(elem) {
              return elem.complete === true ? 'checked': '';
         }
         return model.getList().map(function(elem, index) {
             return {
                val: elem.val,
                checked: isChecked(elem)
            };
        });
    }
    return {
        getDOM: function() {
            return DOM;
        },
        notify: function() {
            const html = templateFnc({ view_list: getData() });
            DOM.list.html(html);
         }
    };          
}

Controller

/**
*  Controllers handle input.  Gets DOM refs from View.
*  then sets event handlers for input text box to add new 
*  item.  Performs register() of both View and itself
*  to set up list checkbox and remove click event handlers
*  after DOM rebuilt.
*/
function ToDoCtrl(view, model) {
    const DOM = view.getDOM();
    // input handler
    DOM.input.blur(() => {
        model.add(DOM.input.val());
    });
    DOM.input.keyup((ev) => {
        if (ev.which == 13 || ev.keyCode == 13) {
            DOM.input.blur();
        }
    });
    model.register(view, this);
    return {
        notify: function() {
            const that = this;
            // checkbox handlers
            DOM.list.find('.input').each(function(index) {
                $(this).click(() => {
                    that.model.complete(index, $(this).is(':checked')); 
                });
            });
            // remove handlers
            DOM.list.find('.remove').each(function(index) {
                $(this).click(() => {
                    that.model.remove(index); 
                });
            });
        };
    };
}

Simple observer pattern abstracts Model’s dependencies to Subject.

/**
*  Simple pull observer pattern for change notification.
*/
function Subject() {
    const observers = [];
    return: {    
        add: function(item) {
            observers.push(item);
        },
        removeAll: function() {
            observers.length = 0;
        },
        notifyObservers() {
            observers.forEach(elem => {
                elem.notify();
            });
        }
    };
}

Initialize templating and MVC objects.

const model = ToDoModel(),
      view = ToDoView(model),
      ctrl = ToDoCtrl(view, model);

So what did MVC buy us? It separated presentation from business logic and decoupled I/O. The Model managed list items as objects with ‘complete’ and value properties. The View requested the model’s data to 1. retrieve item values 2. grey text upon checked box. The controller handled add, remove, and checked events. These separation of roles give structure when scaling such as synching checked values with a database, or using a search box to filter the list while displaying checked boxes.

 Criticism

The observer pattern comes with the standard drawbacks of avoiding hard coded dependencies to decouple components. One can’t look at the source code to easily trace execution flow and instead must use a debugger. Also, cascading updates between parent-child or sibling level components can add complexity if not managed well.



As new frameworks emerge it’s useful to compare them to the mechanics of classic MVC, and in this post we covered how to write MVC using POJOs. I hope this was useful for you.

 References

[1] Performance Impact of Popular JavaScript MVC Frameworks

[2] A Cookbook for Using
Model View Controller

[3] Addy Osmani, Patterns For Large-Scale JavaScript Application Architecture

[4] What’s a Controller Anyway?

[5] Trygve Reenskaug, MVC Notes

[6] Brett Slatkin, Why client-side templating is wrong

 
386
Kudos
 
386
Kudos

Now read this

Designing Normalized SQL Tables

SQL databases are reliable for complex queries, partial updates, transactions, and decoupling data modeling from application specific contexts. In this post we’ll cover how to normalize tables in 1NF, 2NF, and 3NF. We have a table. Name... Continue →