Evolve your app to MVC pattern

1. What is it?

MVC is a pattern/framework decouple application into 3 parts: modal, view, and controller.

  • Modal: the single source of truth of your app, responsible for data input and data output;
  • View: the UI part displayed to users, responsible for display;
  • Controller: bind views to modal organically in a desired way, responsible for handling Modal(automatically resulting in View update)

[Note]: MVC is based on Observer Pattern

2. Why use it?

As app grow, the business logic between data and view become complicated. There could be a good chance that several component use same piece of data. It would be painful to call vanilla JS to update every component one by one.

It would be better if there is a way that we can update all those components in a batch automatically when that data piece changes. Here come in the MVC pattern.

  • View is html code snippet actually on frontend;
  • Modal defines all single source of true for their views
  • Controller will
    • binds all Views to their corresponding Modal, so that Views will update automatically when the Modal changes.
    • via callback passed in, it updatesModal, leading to Views get updated

3. How to use it?

The general way:

  1. Declare modal class prototype, and its APIs, which will be called by controller
  2. Specify to-be-bound modal for view components
  3. pass intention callback tocontroller to specify how to update modal

4. Evolve to MVC pattern (eg: Timer)

We will implement a very simple widget to show how to evolve an app to MVC pattern. The widget is to display whatever we get from API or from callback, and show it inside a div.

  1. No Pattern

    1
    2
    3
    4
    5
    6
    7
    // View Part:
    <div id="div1"></div>

    // Logic Part:
    const data = "This is the data"
    const div = document.getElementById('div1');
    div.innerHTML = data;
  1. Using Observer Pattern

    Now we don’t want to update view by ourselves. Instead, we hope data update will trigger view change automatically. Observer Pattern can be used here to let the view observe the modal, so whenever modal changes, view will get updated automatically.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    function Model(value) {
    this._value = typeof value === 'undefined' ? '' : value;
    this._listeners = [];
    }
    Model.prototype.set = function (value) {
    var self = this;
    self._value = value;
    // model value change will notify regisered callbacks
    // According to JS running principle, we apply callback asynchronously: WON'T Freeze UI
    // or use requestAnimationFrame, rather than setTimout
    setTimeout(function () {
    self._listeners.forEach(function (listener) {
    listener.call(self, value);
    });
    });
    };
    Model.prototype.watch = function (listener) {
    // register callback
    this._listeners.push(listener);
    };

    // View:
    <div id="div1"></div>

    // Logic:
    (function () {
    var model = new Model();
    var div1 = document.getElementById('div1');
    model.watch(function (value) {
    div1.innerHTML = value;
    });
    model.set('hello, this is a div');
    })();
  1. Bind View and Modal

    There is a drawback in the above approach: we need to make modal to watch each related views manually. But the truth is that all those work are duplicated, and conveys the same functionality: bind views to modal, or let modal watch related views.

    We can encapsulate this feature onto modal actually, as follows:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Model.prototype.bind = function (node) {
    // put `watch` and `common callback` inside bind
    this.watch(function (value) {
    node.innerHTML = value;
    });
    };

    // View:
    <div id="div1"></div>
    <div id="div2"></div>

    // Logic:
    (function () {
    var model = new Model();
    model.bind(document.getElementById('div1'));
    model.bind(document.getElementById('div2'));
    model.set('this is a div');
    })();
  1. MVC Pattern

    Now, it is better. We bind view and modal😄~ And we can bind as many as views to a modal as we want😱.

    But still, we need to manually bind view to modal. Is there a way our code can handle it for us, as long as we tell the code which modal the view want to bind.

    Yeah, You might already get it! We can specify modal piece for the view by using HTML attribute, like <div bind='modal1' />. Thus, the code can know the desired modal current view wants to bind.

    We will create an util object that can

    • firstly, automatically bind views to their modals,
    • secondly, receive a callback to update modal in programmer’s desired way
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    function Controller(callback) {
    var models = {};
    // find all views with `bind` attr
    var views = document.querySelectorAll('[bind]');
    // convert views into array
    views = Array.prototype.slice.call(views, 0);
    views.forEach(function (view) {
    var modelName = view.getAttribute('bind');
    // get the modal for this view, or create new one for it
    models[modelName] = models[modelName] || new Model();
    // bind view and its modal
    models[modelName].bind(view);
    });
    // update modals using specified callback
    callback.call(this, models);
    }


    // View:
    <div id="div1" bind="model1"></div>
    <div id="div2" bind="model1"></div>
    // Controller:
    new Controller(function (models) {
    var model1 = models.model1;
    model1.set('this is a div');
    });
  2. The whole MVC

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    function Model(value) {
    this._value = typeof value === 'undefined' ? '' : value;
    this._listeners = [];
    }
    Model.prototype.set = function (value) {
    var self = this;
    self._value = value;
    setTimeout(function () {
    self._listeners.forEach(function (listener) {
    listener.call(self, value);
    });
    });
    };
    Model.prototype.watch = function (listener) {
    this._listeners.push(listener);
    };
    Model.prototype.bind = function (node) {
    this.watch(function (value) {
    node.innerHTML = value;
    });
    };
    function Controller(callback) {
    var models = {};
    var views = Array.prototype.slice.call(document.querySelectorAll('[bind]'), 0);
    views.forEach(function (view) {
    var modelName = view.getAttribute('bind');
    (models[modelName] = models[modelName] || new Model()).bind(view);
    });
    callback.call(this, models);
    }

5. Test our MVC framework

We will use our MVC framework to implement a Timer as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// View:
<span bind="hour"></span> : <span bind="minute"></span> : <span bind="second"></span>

// Controller:
new Controller(function (models) {
function setTime() {
var date = new Date();
models.hour.set(date.getHours());
models.minute.set(date.getMinutes());
models.second.set(date.getSeconds());
}
setTime();
setInterval(setTime, 1000);
});

6. Thoughts

For almost all frameworks like Redux, Flux. It use similiar ways to handle View and Modal. Here we use Observer Pattern to implement MVC.

Based on my understanding, Redux and Flux use Publisher-Subscriber Pattern will handles events better than Observer Pattern.

7. Reference

  1. 30行代码实现Javascript中的MVC
  2. Design Pattern - JS
  3. Learning JavaScript Design Patterns