A GrailsUI Feature Addition, Step-By-Step


This is not a GrailsUI tutorial, this the story of a GrailsUI JIRA feature request, and how it was implemented.


The origin of this JIRA issue was the grails-user mailing list, where someone was asking about the easiest way to navigate on row click with a GrailsUI dataTable tag. There is a solution, but it involves taking advantage of the inherent component accessibility in GrailsUI to add a row click listener after the tag, like this:

Click handling using component accessibility

<script>
    var callback = {
        success: function(request) {
            alert('success');
        },
        failure: function(request) {
            alert('failure');
        }
    };
    // here we access the datatable through the GRAILSUI namespace and attach a custom listener
    GRAILSUI.myDataTable.subscribe(&quot;rowClickEvent&quot;, function(oArgs) {
        YAHOO.util.Connect.asyncRequest('POST', 'my/submission/url', callback, &quot;clicked=&quot;
                + oArgs.target);
    });
</script>

Here, we needed to access the YUI DataTable object that GrailsUI created for us through the GRAILSUI namespace. Then we subscribed to the ‘rowClickEvent’ of the DataTable, and used it to call a URL with data from the click event.

But what we really want to do is much simpler. I just want to be able to click a row and navigate to a specific URL associated with that row. It seems like the GrailsUI dataTable tag should be able to handle this for us. It already has a rowExpansion attribute that will call a URL to populate a newly expanded element on row click, using a ‘dataUrl’ JSON field in the table data (more on row expansion here). It should be easy to set up the dataTable tag to navigate to the URL provided in ‘dataUrl’ instead of using it for a rowExpansion.

So let’s dive into the code to see where we can implement this. The place to start, believe it or not, is with a test. This is where we will define some of our requirements. First of all, I know that the ‘rowExpansion’ attribute is not compatible with the row click navigation feature we want to add. They can’t both be active at the same time. We can’t set up row click handling that expands the row as well as navigating to a new URL. That just doesn’t make sense.

This is a good place for an initial test. Let’s say we expect the user may send us a boolean attribute called ‘rowClickNavigation’, and when true we need expect the dataTable to navigate to a new URL on row click. I think we can say that the rowExpansion and rowClickNavigation attributes cannot both be true simultaneously. This is a case of an invalid call to the API, and bad logic coming from the GSP, so let’s make sure an exception is thrown.

GrailsUI already has a DisplayTagLibDataTableTests.groovy, and this is the place for all DataTable testing. The setUp() of this test will give us a loaded attrs variable and also handle catching anything being shifted into ‘out’… So let’s add this method:

DisplayTagLibDataTableTests.groovy

void testExceptionThrownWhenRowExpansionAndRowClickNavigationBothTrue() {
    attrs.rowExpansion = true
    attrs.rowClickNavigation = true
    def result = shouldFail {
        taglib.dataTable(attrs)
    }
    assertEquals('Wrong Exception message', 'GrailsUIException: \'rowExpansion\' and \'rowClickNavigation\' cannot both be '
            + 'true.  Only one row click handler is allowed.  To fix, remove one, or set one to false.', result)
}

And run the tests…

grails test-app DisplayTagLibDataTable

And we get the expected assertion failure, so now to put the exception in place.

If you look at any of the GrailsUI tag source code, you’ll notice that the first thing done with every tag is a call to grailsUITagLibService.establishDefaultValues(). This method takes a map of default values as its first parameter, the original attrs map as its second, and then an optional list of required parameter names as the third. There is an entry in the default map that sets rowExpansion to false, and I’m going to do the same thing for rowClickNavigation. This ensures that these features will only be used if the user specifies it in the tag attributes.

After establishing default values, you can see there is already one check for erroneous attribute input on sortedBy. We’ll want to do something similar in the same area:

DisplayTagLib.groovy: throwing exception if both are true

// can't have rowExpansion and rowClickNavigation both true
def rowExpansion = attrs.remove('rowExpansion')
def rowClickNavigation = attrs.remove('rowClickNavigation')
if (rowExpansion && rowClickNavigation) {
    throw new GrailsUIException('\'rowExpansion\' and \'rowClickNavigation\' cannot both be '
            + 'true.  Only one row click handler is allowed.  To fix, remove one, or set one to false.')
}

Both attributes are removed from the attrs map because I don’t want them passed into the GRAILSUI.DataTable config object. Because of GrailsUI’s config pass-through, any attribute values that are note removed from the attrs map will be converted into part of a JavaScript literal config object object and passed into the DataTable constructor.

Back to the code, our test is now passing.

It seems to me like there are two options on how to alter the dataTable tag behavior when a row is clicked. We could have the tag write out a listener after it creates the DataTable, which is similar to the way we could have attached a listener using component accessibility at the beginning of this article. But I think there is a better way: by altering the GrailsUI DataTable YUI extension.

There are several instances within GrailsUI where YUI elements are not just created, but extended. The GRAILSUI.DataTable is an example of that, because it extends YAHOO.widget.DataTable, providing row expansion and server-side sorting.

Wouldn’t it be nice if we could just send in a config value to tell the GRAILSUI.DataTable what to do on row click? Maybe it could default to ‘none’, but we could also send in ‘expand’ or ‘navigate’.

If you look at that DataTable source, there is a method called rowSelectionMade, and on line 95 there is a check to see if there is a dataUrl field in the JSON data for the table before attempting a row expansion. This is going to be our sweet spot.

DataTable.js full rowSelectionMade method before

rowSelectionMade: function(args, callback) {
    // if an already expanded section is being clicked, we don't want to proceess a selection so the
    // user can interact with the expansion properly
    var targetIsExpansion = YAHOO.util.Dom.hasClass(args.target,'ymod-expandedData');
    var targetAncestorIsExpansion = YAHOO.util.Dom.getAncestorByClassName(args.target,'ymod-expandedData') == undefined ? false : true;
    var expandedClick = targetIsExpansion || targetAncestorIsExpansion;
    // but if user set to always collapse on expansion click, set it back to false
    if (this.collapseOnExpansionClick) {
        expandedClick = false;
    }
    if( this.get( 'selectionMode' ) == 'single' && !expandedClick) {
        this.collapseAll();
        if (YAHOO.util.Dom.hasClass(args.target, 'yui-dt-selected')) {
            this.unselectAllRows();
        } else {
            this.onEventSelectRow.call( this, args );
        }
        // only click and expand if the response schema defines a data url for row expansion
        var dataUrlIndex = this.getDataSource().responseSchema.fields.join().search('dataUrl');
        if (dataUrlIndex &gt; 0) {
            this.clickAndExpand(args[ 'event' ], callback);
        }
    }
},

If the DataTable knew its row click mode when it was created, then we could add code here that would decide whether to perform the row expansion logic, or just navigate to another URL. Let’s pretend for the moment that there is an attribute of GRAILSUI.DataTable called rowClickMode that will contain either ‘none’, ‘expand’, or ‘navigate’. If we know that, then lets update the last if block in the method:

DataTable.js partial rowSelectionMade after

    if( this.get( 'selectionMode' ) == 'single' && !expandedClick) {
        this.collapseAll();
        if (YAHOO.util.Dom.hasClass(args.target, 'yui-dt-selected')) {
            this.unselectAllRows();
        } else {
            this.onEventSelectRow.call( this, args );
        }
        // ignore everything in none mode
        if (this.rowClickMode == 'none') return;
        // only click and expand if the response schema defines a data url for row expansion
        var dataUrlIndex = this.getDataSource().responseSchema.fields.join().search('dataUrl');
        if (dataUrlIndex &gt; 0) {
            if (this.rowClickMode == 'expand') {
                this.clickAndExpand(args[ 'event' ], callback);
            } else {
                window.location = this.getRecord(this.getSelectedTrEls()[0]).getData().dataUrl;
            }
        }
    }

So now we need to give the GRAILSUI.DataTable a rowClickMode property. So lets slip the attribute in amongst the existing ones:

DataTable.js attributes with rowClickMode

YAHOO.lang.extend(GRAILSUI.DataTable, YAHOO.widget.DataTable, {
    loadingDialog: null,
    collapseOnExpansionClick: false,
    rowClickMode: 'none', // none, expand, or navigate
    customQueryString: null,
...

I’ve given it a default value of ‘none’ so there won’t be any null errors. Now we need to alter the constructor to inspect the incoming config object and set the attribute.

DataTable.js constructor

GRAILSUI.DataTable = function(elContainer, aColumnDefs, oDataSource, queryString, oConfig) {
    if (arguments.length > 0) {
        this.customQueryString = queryString;
        this.loadingDialog = new GRAILSUI.LoadingDialog(elContainer + 'loading', null);
        if (oConfig['collapseOnExpansionClick'] != undefined) {
            this.collapseOnExpansionClick = oConfig['collapseOnExpansionClick'];
        }
        if (oConfig['rowClickMode'] != undefined) {
            this.rowClickMode = oConfig['rowClickMode'];
        }
        GRAILSUI.DataTable.superclass.constructor.call(this, elContainer, aColumnDefs, oDataSource, oConfig);
    }
    this._initSelfListeners();
};

Now we’ve altered the GRAILSUI.DataTable object definition to accept a new config value called ‘rowClickMode’, which will be used to decide how to handle row click events. Excellent. We are only left with the dilemma of how to translate the incoming dataTable tag attributes.

When the user sends a rowExpansion=”true” into the tag, we need to ensure that the config object sent to the GRAILSUI.DataTable constructor contains rowClickMode=”expand”. Likewise, if the user sends rowClickNavigation=”true”, the constructor needs to contain rowClickMode=”navigate”. Otherwise it will default to “none”. Luckily, setting this up is easy:

DisplayTagLib.groovy: setting up the config

// the GRAILSUI.DataTable.rowClickMode is either 'none', 'expand', or 'navigate'
if (rowExpansion) attrs.rowClickMode = 'expand'
if (rowClickNavigation) attrs.rowClickMode = 'navigate'

After adding a default value of rowClickMode=’none’ in the call to establishDefaultValues() at the start of the method, we are done. We’ve removed the attribute values the user sent into the tag to set up the click behavior, and we’ve translated them into a rowClickMode value, and attached it back to the attrs in order for GrailsUI to pass it into the DataTable constructor.

And now GrailsUI 1.0.2 will have a better dataTable tag. Thanks for observing this GrailsUI feature request implementation. Here is the full changeset for this feature implementation.

This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink. Trackbacks are closed, but you can post a comment.

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*
Check out the latest GroovyMag to see an interview with me about the 1.1 release of GrailsUI: