Simplify front-end modules using the MVVM pattern

Auteur
Mark Ettema
Datum

Context

In our project we wanted to extend the ‘Auto complete’ module. This module class was quite big and complex. Besides that, it was not possible to split it up in smaller functionalities. The moment was there to add a new abstraction layer to keep code clear and maintainable. The MVVM pattern looked like a logical step to split up the code, based on separation of concerns. I dove into the benefits of using this pattern and it turns out that in certain situations there is a definite advantage!

Origin of MVVM

This pattern was invented by Microsoft when they were working on Silverlight. A great solution for when you work on a visual interface where you have a lot of event handling and UI state changes.

UI state changes made easy

By using this pattern you always follow the same route to update the view. Update the data, aka state, and render the view with the updated data. The current state of the view is not important and makes it possible to always take the same approach.

MVVM in favor of MVC

In MVC the View retrieves the data itself and directly from the Model. The View has knowledge of the Model and there is logic needed to render. In MVVM the ViewModel transforms the data first into a structure that can be rendered without logic and supplies the View with that data.

Apply to front-end modules

Implementing the pattern in the back-end and front-end can’t be the same as the DOM has its influence. On a detailed level we have to come up with rules that apply to front-end and still stick with the core of MVVM.

The Separation of concerns

MVVM stands for Model – View – ViewModel. The Model and View only have knowledge of themselves and the ViewModel knows both others. There is a clear separation of concerns.

Model

The Model contains all the data needed to render the HTML. The data in the Model represents the current state in the DOM. The data is raw and has no relation to the presentation. That means that there is no tight coupling between Model and View.

View

The View is responsible how to render the HTML. The render method always receives data as input. Furthermore, it handles everything related to the DOM including event binding.

ViewModel

The ViewModel handles the events bound by the View. Every event handler follows the same steps:

  1. Update Model;
  2. Transform data from the Model into View data;
  3. Calls render method on View and supplies the created View data.

It is not mandatory to use a framework to implement MVVM, there it is only a set of rules to guarantee separation of concerns.

Show me code!

We start with the initial HTML that contains the basic functionality, an input field. This way the principle of Progressive Enhancement is applied. The enhancement is handled by the “AddRemove” module.

Initial HTML in the DOM

<div data-module="ui/AddRemove">
    <input />
</div>

Constructing the ViewModel instance

When our application creates an instance of our ViewModel, the ViewModel creates instances of the Model and View, defines the event handlers and calls the render method on the View to apply the first enhancement.

import View from './View';

/**
 * @param {HTMLElement} element
 */
constructor(element) {
    /**
     * @type {AddRemoveView}
     */
    this.view = new View(element, {
        onAddButtonClick: () => this.onAddButtonClick(),
        onRemoveButtonClick: event => this.onRemoveButtonClick(event),
        onInputChange: event => this.onInputChange(event)
    });

    // creation of the Model left out for brevity

    this.view.renderInDOM(this.getViewData());
}

Enhanced HTML in the DOM

After renderInDOM() is executed we have our enhanced HTML rendered in the DOM.

<div data-module="ui/AddRemove">
    <ol>
        <li data-row="0">
            <input value="" />
        </li>
    </ol>
    <button data-add-button disabled>+<button>
</div>

Constructing the View instance

The View knows how the HTML looks like and binds the events.

import { delegate } from 'utils/DOMUtils';

/**
 * @param {HTMLElement} element
 * @param {ViewOptions} options
 */
constructor(element, options) {
    /**
     * @type {HTMlElement}
     */
    this.element = element;

    this.element.addEventListener('click', delegate(
      '[data-add-button]',  options.onAddButtonClick)
    );

    [...]
}

Rendering

A method is called with the View data that maps directly onto the HTML to render and places the returned rendered HTML into to the DOM.

/**
 * @param {ViewData} viewData
 */
renderInDOM(viewData) {
    this.element.innerHTML = this.renderTemplate(viewData); 
}

/**
 * @param {ViewData} viewData
 * @
 */
renderTemplate(viewData) {
    const otherRows = this.view.renderOtherRowsListItemsTemplate(viewData.otherRows);
    const disabledAttr = viewData.addButtonDisabled ? 'disabled' : '';
    return `
        <div data-module="ui/AddRemove">
            <ol>
                <li data-row="0">
                    <input value="${firstRow.value}" />
                </li>
                ${otherRows}
            </ol>
            <button data-add-button ${disabledAttr}>+<button>
        </div>`;
}

Let’s trigger an event!

After the DOM is rendered including the enhancement, the user is able to interact with it. Let’s assume the user clicks on the ‘add’ button. As configured in the constructor function of the ViewModel, the onAddButtonClick() method is executed after the user triggers the corresponding event as bound in the constructor function of the View. It will execute the three steps like mentioned before:

  1. Update Model;
  2. Transform Model data into View Data;
  3. Supply View data to View render method.
onAddButtonClick() {
    this.model.addItem();
    const viewData = this.getViewData();
    this.view.renderInDOM(viewData);
}

HTML after state transformation

<div data-module="ui/AddRemove">
    <ol>
        <li data-row="0">    
            <input value="" />
        </li>
        <li data-row="1">
            <input value="" />
            <button data-remove-button>-</button>
        </li>
    </ol>
    <button data-add-button disabled>+<button>
</div>

Testability

Are you afraid that the amount of state in the Model reduces the testability of your module? The render methods has data as input and HTML as output so they are pure functions. This way they can be tested without interference of the DOM with different setups of View data!

Conclusion

The next time you are given a framework like AngularJS as requirement, think twice and determine if it might fit ‘You Aren't Gonna Need It’, since a framework comes with its own complexity. The MVVM pattern came out as really suitable in certain situation. Try it out yourself and enjoy!

Tags

progressive enhancement Design patterns