Medryx Observations

June 9, 2008

Building (and Testing) Dojo Widgets — MVC?

Filed under: dojo, javascript, oop, unit testing — Tags: , , , , , — maulin @ 8:04 am

There are plenty of tutorials out there about Dojo widgets, so I won’t write another one. But I would like to document here my philosophy for writing widgets, allowing for separation of concerns and perhaps even an MVC architecture.

In my mind, a widget is a hybrid of view component and model component. It builds a UI, then receives events on that UI that it passes on the model. But this can get messy, especially for testing, debugging, and changes later on down the road. So I propose that we separate the widget into its elemental components, and then only combine them in the “final widget”. Here’s what I mean:

Lets create widget called TeamList, that creates a list of teams. When you click on a team you get a dialog with team details.

/widgets
---/ui
------/tests
---------/all.js
---------/runTests.html
---------/TeamListTest.js
------/_TeamList
---------/TeamList.html
---------/TeamListView.js
---------/TeamListController.js
---------/tundra.css
------TeamList.js
---/themes
------/tundra.css

As you can see, I actually seperate my widgets into several files:

  1. TeamList.html: the standard widget template
  2. TeamListView.js: a class that DOES NOT derive from the usual _Widget or _Templated, but that “pretends to”.

    This class is used to manipulate everything related to the UI, and to create the events that the widget will generate (or translate the low level user events to more useful ones for the application).

    The key is that none of the “high-level” events will trigger ANYTHING to happen in the model. That is the job of the controller.

    This makes this class quite isolated, and fairly easy to test and change, as long as it adheres to its contract of high-level events that it will trigger.

    Here is the code for my TeamListView:

     
    dojo.declare("medryx.widgets.ui._TeamList.TeamListView", medryx.AbstractBase, {
        //*** attributes
        //filter the teams
        filter:null,
     
        //sort the teams. this can be an array of sort objects (see AbstractDao.sort)
        //or can be a comma-seperated list of fields (simpler, but less flexible)
        sort:null,
        headerContent:"",
    	footerContent:"",
     
        //*** internal
        templatePath: dojo.moduleUrl("medryx","widgets/ui/teamList/teamList.html"),
        controller:null,
        teams:null,
     
        //** nodes defined in the template -- only defined here so we remember them
        list:null,
     
        /**
         *  abstract method
         *  MUST return a valid initialized controller
         */
        $buildController:null,
     
        postCreate:function() {
            this.controller = $buildController();
            this.loadTeams({
                filter: this.filter,
                sort: this.sort
            });
        },
       /**
         * delegate to the controller.
         */
        loadTeams:function() {
            var deferred = this.controller.loadTeams(arguments);
            deferred.addCallback(dojo.hitch(this, this.displayTeams, this.list);
        },
       displayTeams:function(list, teams) {
    	dojo.forEach(teams, function(team) {
    		var li = document.createElement("li");
    		li.innerHTML = team.getName();
    		list.appendChild(li);
    		dojo.connect(list, "onclick", dojo.hitch(this, "teamClicked", team));
    	});
       },
       teamClicked:function(team, event) {
       }

    So lets point out some interesting things about this “View”. Notice that it does not do anything about actually loading teams, or handling what happens once a team gets clicked. It just translates the low level click event to a high level teamClicked event that passes the team of interest to the event.

    Next, notice that while we implement postCreate(), we are not inheriting (yet) from _Widget or _Templated. Here’s why. Lets say I want to test this class. If it had already inherited from these classes, it would automatically call the lifecycle events of the class. But I don’t necessarily want that to happen… yet. I want to call my postCreate method manually and run my tests to be sure everything in this class is working right, before I start wiring things up.

    One other small point — look at displayTeams. Notice how I actually pass the list to which I want to attach the items to this method. Obviously I could use “this.list” throughout displayTeams, and then I wouldn’t have to pass the argument. But stylistically, I find it easier to write tests for methods that are “self contained” and have as little reference to “this.*” as possible. If I hadn’t passed the list as a parameter, I would have had to remember in my tests to set this.list to a new list before running my test. And you can easily forget to do that. In other words, I try to make my widgets as “stateless” as possible in the View. I like to let the final widget inject all the state that is necessary.

    Here is the (very simple) controller class:

    dojo.declare("medryx.widgets.ui._TeamList.TeamListController", null, {
        view:null,
        constructor:function(view) {
            this.view = view;
            dojo.connect(view, "teamClicked", this, "pageTeam");
        },
     
     /**
      * return a deferred that will callback for all teams
      * that are to be display. the arguments are filter and sort
      * either as parameters or as kw arguments
      */
        loadTeams:function() {
            return medryx.dao.TeamsDao.getAll(arguments);
        },
     
        pageTeam:function(team) {
            var pageForm = new medryx.widgets.ui.PageForm();
            pageForm.setTeam(team);
            pageForm.show();
        }
    });

    This is where I connect this widget to other layers of the application. So all interactions outside of the widget should occur through this controller. Notice how I connect up the “teamClicked” event to a “pageTeam” function. Another advantage of separating the view from the controller is that allI have to do is a use a different controller, and I can get a totally different widget. Again, this is a fairly easy class to test .

    So where does this all come together? In medryx.widgets.ui.TeamList, of course!

    dojo.declare("medryx.widgets.ui.TeamList", [dijit._Widget, dijit._Templated, medryx.widgets.ui._TeamList.TeamListView], {
    	$buildController:function() {
                 return new medryx.widgets.ui._TeamList.TeamListController	(this);
              }
    });

    All this does is create the actual widget class, mixin the _Widget, _Templated to allow the magic of dojo to take over, and to define the controller that will be used.

Powered by WordPress