Tuesday, October 30, 2012

MVC for client-side Javascript with DeftJS

This post is a cross post from the Rally Engineering Bloghttp://www.rallydev.com/community/engineering/mvc-client-side-javascript-deftjs

If you've worked with Javascript for any significant amount of time, you've likely run across tightly coupled UI/logic code that is difficult to test, debug, reuse, and modify. In Javascript land, this problem most often manifests itself as AJAX calls embedded inside of GUI components. While this pattern may work OK for small projects or projects using server-side generated html augmented with 'progressive enhancement' Javascript, it quickly turns into spaghetti code for large Javascript client applications. Our codebase usually does a good job of separating concerns, but occasionally we have logic sneaking into our UI code. Below is an example from a UI component that adds a new business artifact.

    Ext.define('Rally.ui.AddNew', {
        requires: ['Rally.ui.Button'],
        extend: 'Ext.Container',


        _onAddClicked: function() {
            var record = Ext.create(this.models[this._getSelectedRecordType()]),
                params = {fetch: true};

            record.set('Name', this.down('#name').getValue());
            if (this.fireEvent('beforecreate', this, record, params) !== false) {
                    message: 'Creating ' + record.self.displayName + '...'

                    requester: this,
                    callback: this._onAddOperationCompleted,
                    scope: this,
                    params: params

This component responds to a button click by creating an Ext record object and saving it by performing an AJAX call to Rally's REST API. This anti-pattern makes code reuse and testing very difficult. I can't reuse the UI with different logic, and I can't reuse the logic with a different UI. Testing the UI requires mocking out the business logic and AJAX calls, and testing the business logic and AJAX calls requires mocking out the UI. This component is clearly a candidate for being refactored using MVC to separate the logic and UI into reusable and testable units.

Enter DeftJs. DeftJs is an MVC library written specifically to work with Sencha's Ext class system. The small library makes it easy to refactor existing Ext components by adding three new features to the Ext environment:

  • Dependency injection (similar to Spring, allows dependencies to be instantiated and injected when required)

  • View Controllers (the bridge between UI components and model logic)

  • Promises/Deferreds (makes it easier to work with maybe asynchronous operations)

  • Each of Deft's features is described in detail on their GitHub page, so I will focus on how to use the features together to architect an application. The diagram below represents an architecture that keeps separation of concerns clear, while utilizing Ext's core strengths, and being backwards compatible with existing components that are not MVC.

    DeftJs Separation of Concerns

    So how does this architecture affect the code of our AddNew component example? The controller is attached to the component by including an extra mixin and property in the component's definition, and all of the business logic is removed from the component:

        Ext.define('Rally.ui.AddNew', {
            requires: ['Rally.ui.Button'],
            extend: 'Ext.Container',
            mixins: [ 'Deft.mixin.Controllable' ], // enable controller
            controller: 'Rally.deft.controller.AddNewController', // controller class to attach to view

           ... ui logic only ...

    The controller class (in Coffeescript) wires the component to business logic encapsulated in Action objects:

    Ext.define 'Rally.deft.controller.AddNewController',
      extend: 'Deft.mvc.ViewController'
      mixins: ['Deft.mixin.Injectable'] # enable dependency injection
      inject: ['createRecordAction'] # inject createRecordAction attribute

          # register handlers for view events here
          submit: '_onSubmit' .

      # this method called when the view component fires a 'submit' event
      _onSubmit: (cmp, model, recordName) ->
        view = @getView() # Deft automatically creates an accessor to the view
        view.setLoading true

        # injected properties (by default) are assigned to an instance.
        # variable with the same name as the injected property
        # invoke the injected action's method to create a new record
        promise = @createRecordAction.createRecord
          model: model
            Name: recordName

        # a Promise object represents an operation that may or may not be asynchronous
          success: =>
            # record creation was successful, reset view
        promise.always =>
          # remove loading mask and refocus view, whether the operation was successful or not
          view.setLoading false

    Notice that there are no AJAX calls in either the view or the controller. The AJAX logic lives in an Action class. Each Action returns a Deft.promise.Promise object, so that callers don't have to worry about whether or not the operation is asynchronous. Promise objects can also be chained or grouped together, which makes it easy to compose complex Actions from multiple simple Actions. The action code is listed below.

    Ext.define 'Rally.deft.action.CreateRecord',
      mixins: ['Deft.mixin.Injectable'] # enable dependency injection
      inject: ['messageBus'] # inject messageBus property

      createRecord: (options) ->
        # the 'deferred' is the private part of the deferred/promise pair
        # the deferred object should not be exposed to callers
        deferred = Ext.create 'Deft.Deferred'

        record = Ext.create options.model, options.data

        # this Ext call initiates an AJAX operation
          callback: (record, operation) =>;
            @_onRecordSave(record, operation, options, deferred)

        # return a promise object to the caller

      _onRecordSave: (record, operation, options, deferred) ->
        if operation.success
          # call a method on an injected property.
          # listeners can subscribe to the messageBus and be
          # notified when an object is created
          @messageBus.publish Rally.Message.objectCreate, record, this

          # mark the deferred/promise pair as successful
          deferred.resolve record
          # mark the deferred/promise pair as failed
          deferred.reject operation

    Now that we have our logic code separated out, testing becomes much easier. The unit test below tests the functionality of a composed Action. All dependencies and AJAX calls are mocked out, so that the test is specific to the functionality of the class being tested. The syntax of the test assumes using the Jasmine testing framework and sinon.js mocking library.

    describe 'CreateRecordOrOpenEditor', ->

      beforeEach ->
        # Reset injectors to make sure we have a clean slate before running test

        # Mock dependencies needed for all tests.
          createRecordAction: value: {}
          openCreateEditorAction: value: {}
          messageBus: value:
            publish: @stub

      describe 'when createRecordAction fails with a validation error', ->

        beforeEach ->
          # mock failed AJAX operation
              createRecord: ->
                deferred = Ext.create 'Deft.Deferred'
                deferred.reject # immediately reject promise to simulate failed AJAX
              openCreateEditor: @stub

        it 'should open editor', ->
          createRecordOrOpenEditorAction = Ext.create 'Rally.deft.action.CreateRecordOrOpenEditor'


          sinon.assert.calledOnce createRecordOrOpenEditorAction.openCreateEditorAction.openCreateEditor

        it 'should publish displayNotification message', ->
          createRecordOrOpenEditorAction = Ext.create 'Rally.deft.action.CreateRecordOrOpenEditor'


          sinon.assert.calledOnce createRecordOrOpenEditorAction.messageBus.publish
          sinon.assert.calledWith createRecordOrOpenEditorAction.messageBus.publish, Rally.Message.displayNotification

    This experiment with DeftJs was successful in that we were able to refactor this component to be more maintainable, reusable, and testable, without requiring rewrites to other parts of the application. We're hoping to implement this architecture in our production code base to clean up some of the areas where concerns bleed together.