Dojo FAQ: Testable store-backed widget

By on January 14, 2015 1:11 pm

DojoFAQ

As object stores are frequently used in Dojo-based applications, developers often ask about a good way to utilize stores in their custom widgets. Here we present a common pattern for doing just that in a two-stage approach, starting simple.

A store-based widget needs two things at minimum: a way to set the store and a way to render items from the store. We start with a mixin that provides those elements. Note that standard documentation is omitted for brevity.

define([
  'dojo/_base/declare',
  'dojo/dom-construct'
], function(declare, domConstruct) {
  return declare(null, {
    store: null,

    _setStoreAttr: function (store) {
      this._set('store', store);

      if (store) {
        this._renderItems(this.store.query({}));
      }
    },

    _renderItems: function (queryResults) {
      queryResults.forEach(function (item) {
        var renderedItem = this._renderItem(item);
        domConstruct.place(
          renderedItem,
          this.containerNode || this.domNode
        );
      }, this);
    },

    _renderItem: function (item) {
      // render the item and return its DOM node
    }
  });
});

Simply incorporate this mixin and provide an implementation for the _renderItem method and you will have a store-based widget. This may work well for simple widgets, but the implementation has considerable shortcomings. It does not support setting a different store, configuring the store query, or returning widgets from _renderItem. Let’s address these one at a time.

We start by adding support for setting a new store. Setting a store works the same as before but removes existing rendered items before rendering items from a new store.

_setStoreAttr: function (store) {
  this._set('store', store);

  this._destroyItems();
  if (store) {
    this._renderItems(this.store.query({}));
  }
},

_destroyItems: function () {
  domConstruct.empty(this.containerNode || this.domNode);
},

It’s not very useful to create a store-based widget without a configurable query. The following changes add support for setting a store query. Both the store and query setters need to destroy existing items and render new ones, so we encapsulate this in a _render method that is used by both.

_setStoreAttr: function (store) {
  this._set('store', store);
  this._render();
},

query: {},
_setQueryAttr: function (query) {
  this._set('query', query);
  this._render();
},

_render: function () {
  // destroy any existing items
  this._destroyItems();
  this.store && this._renderItems(
    this.store.query(this.query);  
  );
},

Finally, we update _renderItems and _destroyItems with support for items rendered as widgets:

_renderItems: function (queryResults) {
  queryResults.forEach(function (item) {
    var renderedItem = this._renderItem(item);
    if ('placeAt' in renderedItem) {
      renderedItem.placeAt(this);
    }
    else {
      domConstruct.place(
        renderedItem,
        this.containerNode || this.domNode
      );
    }
  }, this);
},

_destroyItems: function () {
  // preserve the item DOM while destroying widgets
  // so we can remove all item DOM nodes in one shot
  var preserveDom = true;
  this.destroyDescendants(preserveDom);

  domConstruct.empty(this.containerNode || this.domNode);
}

Maintainability and testability

Designing widgets to be backed by the Dojo store API (as opposed to having widgets make direct XHR calls to data services) decouples data fetching logic from widget logic, enhancing maintainability and testability. Changes to the data service do not require updates to the widgets that consume the data, just updates to the custom stores that fetch the data from the server. The stores can be tested independently of the widgets, and the widgets can be tested with mock stores, enabling them to be tested in isolation from the data service.

The final result is:

define([
    'dojo/_base/declare',
    'dojo/dom-construct'
], function(declare, domConstruct) {
    return declare(null, {
        // summary:
        //        A mixin for creating a store-based widget

        store: null,
        query: null,

        _buildRendering: function () {
            this.inherited(arguments);
            this._render();
        },

        _postCreate: function () {
            this.inherited(arguments);

            this.own(
                this.watch('store', this._render),
                this.watch('query', this._render)
            );
        },

        _render: function () {
            // summary:
            //        Render the widget contents

            // destroy any existing items
            this._destroyItems();
            this.store && this._renderItems(
                this.store.query(this.query || {})
            );
        },

        _renderItems: function (/*Array*/queryResults) {
            // summary:
            //        Render query results
            // queryResults:
            //        The results to render

            queryResults.forEach(function (item) {
                var renderedItem = this._renderItem(item);

                // look for `placeAt` method to support items rendered as widgets
                if ('placeAt' in renderedItem) {
                    renderedItem.placeAt(this);
                }
                else {
                    domConstruct.place(
                        renderedItem,
                        this.containerNode || this.domNode
                    );
                }
            }, this);
        },

        _renderItem: function (/*Object*/item) {
            // summary:
            //         Render an item
            // item:
            //        The item to render
            // returns: DomNode|dijit/_WidgetBase
        },

        _destroyItems: function () {
            // summary:
            //        Destroy currently rendered items

            // preserve the item DOM while destroying widgets
            // so we can remove all item DOM nodes in one shot
            var preserveDom = true;
            this.destroyDescendants(preserveDom);

            domConstruct.empty(this.containerNode || this.domNode);
        }
    });
});

We now have a mixin for store-based widgets that supports dynamically changing the store, configurable queries, and rendering to either DOM nodes or widgets.

Conclusion

This mixin provides a common basis for data-driven widgets based on a consistent data API (the Dojo store API) to create an architecture that is easy for developers to work with, maintainable, and testable. In a follow-up post, we will develop a similar mixin based on dstore, the next-generation data store API from SitePen.

Learning more

SitePen covers widget architecture, advanced store usage and much more in our Dojo workshops offered throughout the US, Canada, and Europe, or at your location. We also provide expert JavaScript and Dojo support and development services. Contact us for a free 30 minute consultation to discuss how we can help.

Comments