Introducing React ReExt – Sencha Ext JS Components in React! LEARN MORE

Building Maintainable Controllers in Ext JS Apps

May 21, 2015 126 Views
Show

We started using Ext JS 4 in late 2011 at eMortgage Logic. We didn’t know how to write Ext JS applications properly, but we eventually got the hang of making the application do what we wanted. That didn’t mean we were architecting them well, however. Our final application was handled by a dozen massive controllers, while we often left views to be a simple set of configs. Controllers did everything. Over time, they became harder to maintain, and it felt like we were organizing our code poorly.

Eventually, we started to make our views be a little bit smarter, and at some point along the line, something clicked with me. I finally felt I knew where to draw the line between controller and view. To explain this newfound knowledge with my team, I wrote the following guide to explain, in a simple way, how we could tear down our massive controllers and replace them with code that had logical separation and better maintainability. This not only benefitted us in the short term with Ext JS 4, but would allow us to more closely mirror Ext JS 5 ViewControllers for an easier upgrade path.

In this article, we’ll build a small controller and simple view with a few requirements, then go through the steps of decoupling them.

Our end goal is to create a controller and view capable of:

  • providing a form for the user to enter their favorite hobby.
  • saving the hobby to their user record.
  • updating the message displaying the user’s name and favorite hobby.

You Had Me at ‘Hello’

View Related Fiddle

We will build a view to allow the current user to see and update their favorite hobby. This view will use the “build” method, so it’s easier to work with later.

app/view/Hobby.js

Ext.define('Example.view.Hobby', {
    extend: 'Ext.form.Panel',
    alias: 'widget.hobbyview',
    initComponent: function () {
        var me = this;
  
        me.items = me.buildItems();
  
        me.callParent(arguments);
    },
    buildItems: function () {
        return [
            {
                xtype: 'textfield',
                name: 'FavoriteHobby'
            },
            {
                xtype: 'button',
                itemId: 'save',
                text: 'Save'
            },
            {
                xtype: 'component',
                itemId: 'hobbyTpl',
                tpl: '{Name}\'s favorite hobby is {FavoriteHobby}'
            }
        ];
    }
});

Now, we’ll build a controller to manage our view.

app/controller/Hobby.js

Ext.define('Example.controller.Hobby', {
    extend: 'Ext.app.Controller',
    init: function () {
        this.control({
            'hobbyview button#save': {
                click: this.onSaveButtonClick
            }
        });
    },
    onSaveButtonClick: function (button) {
        var form = button.up('form'),
            values = form.getValues(),
            user;
 
        // We will assume the application provides a way to get the currently logged-in user.
        user = this.getApplication().getCurrentUser();
 
        user.set(values);
        user.save();
 
        form.down('#hobbyTpl').update({
            Name: user.get('Name'),
            FavoriteHobby: values.FavoriteHobby
        });
    }
});

Despite being a very simple controller, it is incredibly aware of the view.

The controller knows…

  • the view has a button with an itemId of “save”.
  • the button is a child of a form.
  • how to get values from the view’s form.
  • the form has a child with an itemId of “hobbyTpl” and how to update it.
  • the form has no concept of a user.

Everything in the controller relies entirely on the view’s save button, and the structure of the view itself. This causes the controller and view to be tightly-coupled. For example, if the save button ever moves outside of the form component, the controller would have to be rewritten. Likewise, if the itemId of the “hobbyTpl” component were to change, the controller would have to be updated. Any seemingly minor change in the view could potentially break the controller.

You’re Tearing Me Apart

View Related Fiddle

Now that we’re aware of some of the problems with the controller, we can begin to fix them. We’ll start by letting the view be aware of the user it’s related to.

app/view/Hobby.js

Ext.define('Example.view.Hobby', {
    // ...
 
    bindUser: function (record) {
        this.userRecord = record;
    },
    getUser: function () {
        return this.userRecord;
    }
    // ...
});

By setting up some methods related to our user, we can easily make adjustments to the view methods without needing to modify the controller. With these additions, we can make the view aware of a user record by passing it in on instantiation, or by calling bindUser later. We can also get the bound user, which will be needed to make the controller less aware.

Now, we can let the controller allow the view to manage which user gets data saved to it.

app/controller/Hobby.js

Ext.define('Example.controller.Hobby', {
    // ...
    onSaveButtonClick: function (button) {
        var form = button.up('form'),
            values = form.getValues(),
            user = form.getUser();
 
        user.set(values);
        user.save();
 
        form.down('#hobbyTpl').update({
            Name: user.get('Name'),
            FavoriteHobby: values.FavoriteHobby
        });
    }
});

That’s certainly better, and one thing is off the list. The controller is still aware of the structure of the view, but now it is far more flexible.

We can cross something off of our list:

The controller knows…

  • the view has a button with an itemId of “save”.
  • the button is a child of a form.
  • how to get values from the view’s form.
  • the form has a child with an itemId of “hobbyTpl” and how to update it.
  • the form has no concept of a user.

Templates? We Don’t Need [to be aware of] No Stinkin’ Templates

View Related Fiddle

Our view is still left wondering why the controller can’t mind its own business, and we can make it a little less nosy by removing its knowledge of the template.

The view wants its template to be updated when the save button is clicked, so an event handler needs to be added to accomplish that.

app/view/Hobby.js

Ext.define('Example.view.Hobby', {
    // ...
    onSaveClick: function () {
        var me = this,
            values = me.getValues(),
            user = me.getUser();
 
        me.down('#hobbyTpl').update({
            Name: user.get('Name'),
            FavoriteHobby: values.FavoriteHobby
        });
    },
    buildItems: function () {
        var me = this;
 
        return [
            {
                xtype: 'textfield',
                name: 'FavoriteHobby'
            },
            {
                xtype: 'button',
                itemId: 'save',
                text: 'Save',
                listeners: {
                    click: me.onSaveClick,
                    scope: me
                }
            },
            {
                xtype: 'component',
                itemId: 'hobbyTpl',
                tpl: '{Name}\'s favorite hobby is {FavoriteHobby}'
            }
        ];
    }
});

Now that we’ve let the view handle more view tasks, we can simplify the controller even more:

app/controller/Hobby.js

Ext.define('Example.controller.Hobby', {
    // ...
    onSaveButtonClick: function (button) {
        var form = button.up('form'),
            values = form.getValues(),
            user = form.getUser();
 
        user.set(values);
        user.save();
    }
});

Unfortunately, it’s still aware of a lot of view components, but at least one more thing is off the list:

The controller knows…

  • the view has a button with an itemId of “save”.
  • the button is a child of a form.
  • how to get values from the view’s form.
  • the form has a child with an itemId of “hobbyTpl” and how to update it.
  • the form has no concept of a user.

What We’ve Got Here is a Failure to Communicate

View Related Fiddle

It’s time to knock out all of our remaining problems in one fell swoop, by allowing communication from the view to the controller through events. The only thing the controller will know is that it gets data, and it needs to save it.

The view already has gathered all of the data that the controller needs in our onSaveClick event handler, so we’ll just fire an event from there.

app/view/Hobby.js

Ext.define('Example.view.Hobby', {
    // ...
    onSaveClick: function () {
        var me = this,
            values = me.getValues(),
            user = me.getUser();
 
        me.fireEvent('save', me, values, user);
 
        me.down('#hobbyTpl').update({
            Name: user.get('Name'),
            FavoriteHobby: values.FavoriteHobby
        });
    },
    // ...
});

This small change makes the controller much simpler. It is only aware that the view exists, and that it fires a “save” event.

app/controller/Hobby.js

Ext.define('Example.controller.Hobby', {
    extend: 'Ext.app.Controller',
    init: function () {
        this.control({
            'hobbyview': {
                save: this.onSave
            }
        });
    },
    onSave: function (view, values, user) {
        user.set(values);
        user.save();
    }
});

Now, we finally have a controller that knows nothing about the view structure. A controller that only saves data, and a view that only displays data.

As long as the view continues to fire a “save” event, it can now be heavily modified without changing the controller at all.

The controller knows…

  • the view has a button with an itemId of “save”.
  • the button is a child of a form.
  • how to get values from the view’s form.
  • the form has a child with an itemId of “hobbyTpl” and how to update it.
  • the form has no concept of a user.
  • only what it needs to know.

Smarter Views are Focused and Easier to Maintain and Test

Understanding how to break apart a mammoth controller has really helped our team to manage classes in a better way. Not only are our controllers and views more focused and maintainable, they’re easier to test, and closer to the Ext JS 5 ViewController functionality. Allowing our views to be smarter also makes it easier to reuse them elsewhere in the application.

We’ve learned that:

  • views are fully capable of managing their own data.
  • controllers should be able to be written without knowledge of the internals of a view.
  • custom events are perfect for communicating from a view to a controller.