Monday, June 30, 2014

This is a cross-post from the Rally Engineering Blog.

Rally publishes a public javascript AppSDK to give customers the ability to easily access their user story data within custom applications. In order to make the SDK components more reusable in different contexts and provide easier application development, there must be a clear separation between view code and model code within the SDK.

ExtJs with MVC

First off, "MVC" in this post refers to unopinionated, generic MVC. View == UI, Model == application logic, Controller == glue between View and Model. "Component" refers to a class that extends Ext.Component.

There are two popular ways to implement MVC with ExtJs applications. Sencha provides an MVC implementation distributed with ExtJs and DeftJs is a third-party library. I have experimented with both of these MVC implementations, and found them lacking.

Sencha's built-in MVC implementation requires a specific directory structure and configuration with Ext.application. These restrictions make it difficult to adapt the pattern to existing code bases.

Sencha's controllers also encourage code that listens for events fired from a Container's child Components. This is a code smell. Controller code should never "know" about child Components, because refactorring the UI structure will break the controller. Replacing a text input with a slider shouldn't force you to change controller code.

DeftJs is easier to adapt to existing code, but there are some odd object lifecycle issues that make it difficult to work with. Components must mix in Deft's Controllable class and DeftJs controllers must be configured when a Component is defined, rather than when it is instantiated. If you find the need to re-use a Component with two different controllers, you will need to create two separate classes, each defined with a different DeftJS controller.

A Deft controller's init method is not called until after the Component has been rendered, which makes things rather challenging if the controller needs to hook into any pre-render events such as staterestore or render.

Plugins as Controllers

I've found that standard Ext Plugins are a great alternative to Sencha and DeftJs controllers. Plugins fit nicely into the Component life cycle. They can be configured at instantiation time, so reusing the same UI with different behavior is trivial.

Multiple Plugins can be configured for a single Component. This may seem like an odd property for a controller, but it's actually quite nice to split code into multiple controllers for Components that have many different behaviors.

A Plugin's init method is called during the Component's constructor call, so it can respond to any Component events fired during or after the Component's initComponent call. A Plugin's destroy method is called during the Component's destroy call, which allows for easy cleanup of listeners and models orchestrated by the Plugin.

So how does it Work?

Wire up controller Plugins just like any other Plugin.

    /**
     * Controller Plugin
     */
    Ext.define('MyViewController', {
        extend: 'Ext.AbstractPlugin',
        alias: 'plugin.myviewcontroller',

        /**
         * @cfg {Ext.data.Store}
         */
        store: null,

        init: function(cmp) {
            // add listeners to respond to UI events
            cmp.on('save', this.onSave);
        },

        onSave: function(cmp, data) {
            // respond to UI events by calling public view and model methods
            cmp.setLoading(true);
            this.store.add(new this.store.model(data))
            this.store.sync({
                success: function() {
                    cmp.setLoading(false);
                    cmp.showNotification('Record Saved');
                }
            });
        }   
    });
    /**
     * View Component
     */
    Ext.define('MyView', {
        extend: 'Ext.Component',
        alias: 'widget.myview',

        items: [{
            xtype: 'button',
            label: 'Save'
        }],

        initComponent: function() {
            this.callParent(arguments);

            this.addEvents(['save']);

            // button click is abstracted into semantic event to avoid
            // controller having to know UI implementation details
            this.down('button').on('click', function() {
                this.fireEvent('save', {...});
            });
        },

        showNotification: ...
    });
    // instantiate view with controller configuration
    var cmp = container.add('myview', {
        plugins: [{
            ptype: 'myviewcontroller',
            store: myStore
        }]
    });

Components architected with a plugin controller like the example above become easier to maintain and easier to consume. This method of adding controllers to components is relatively painless to adapt to existing code. We are actively refactoring legacy code within Rally's SDK to use this architecture.