How to Use AJAX in a Fragment

Let's say that we want to allow the user to reload the table of encounters by clicking on a "refresh" button, without reloading the entire page. We need to do this using AJAX. (This is a bit of a contrived example. A more realistic example would be to let the user choose a date from a dropdown, and have the table refresh with encounters on that date. The mechanism to support both of these is the same.)

The UI Framework supports "Fragment Actions" which are basically methods in the fragment's controller class that are easy to invoke from views. They aren't required to be asynchronous, but they are particularly helpful for AJAX interactions. So we add a getEncounters method to our fragment controller that fetches the encounters for a given date, and returns them as a JSON list:

package org.openmrs.module.emr.fragment.controller; import org.openmrs.Encounter; import org.openmrs.api.EncounterService; import org.openmrs.ui.framework.SimpleObject; import org.openmrs.ui.framework.UiUtils; import org.openmrs.ui.framework.annotation.FragmentParam; import org.openmrs.ui.framework.annotation.SpringBean; import org.openmrs.ui.framework.fragment.FragmentModel; import org.springframework.web.bind.annotation.RequestParam; import java.util.Calendar; import java.util.Date; import java.util.List; public class EncountersTodayFragmentController { private Date defaultStartDate() { Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); return cal.getTime(); } private Date defaultEndDate(Date startDate) { Calendar cal = Calendar.getInstance(); cal.setTime(startDate); cal.add(Calendar.DAY_OF_MONTH, 1); cal.add(Calendar.MILLISECOND, -1); return cal.getTime(); } public void controller(FragmentModel model, @SpringBean("encounterService") EncounterService service, @FragmentParam(value="start", required=false) Date startDate, @FragmentParam(value="end", required=false) Date endDate) { if (startDate == null) startDate = defaultStartDate(); if (endDate == null) endDate = defaultEndDate(startDate); model.addAttribute("encounters", service.getEncounters(null, null, startDate, endDate, null, null, null, null, null, false)); } public List<SimpleObject> getEncounters(@RequestParam(value="start", required=false) Date startDate, @RequestParam(value="end", required=false) Date endDate, @RequestParam(value="properties", required=false) String[] properties, @SpringBean("encounterService") EncounterService service, UiUtils ui) { if (startDate == null) startDate = defaultStartDate(); if (endDate == null) endDate = defaultEndDate(startDate); if (properties == null) { properties = new String[] { "encounterType", "encounterDatetime", "location", "provider" }; } List<Encounter> encs = service.getEncounters(null, null, startDate, endDate, null, null, null, null, null, false); return SimpleObject.fromCollection(encs, ui, properties); } }

We've added another public method in our controller class. (Any public method on a fragment controller can be accessed as a fragment action.) We know we want to return JSON, but the UI Framework actually takes care of this for us: we just have to return a simple Object, without any hibernate proxies. The last line of this method shows a convenience method that does this for a collection, and I welcome improvements to the SimpleObject class.

The method signature of a fragment action method can be flexible, although the available options are more limited than those on the main controller() method, since a fragment action is just an HTTP request, without the context of a page or a fragment configuration. Most powerfully, we can use Spring's @RequestParam annotation to automatically convert HTTP parameters to our desired datatypes. (See Flexible Method Signatures for UI Framework Controller and Action Methods for complete documentation of allowed parameter types and return types.)

That's all. The server-side definition of our AJAX interaction is quite simple--it relies on the UI Framework to do all the busy work.

Now we need to make our encountersToday.gsp view call this method, and redraw the table when the results come back asynchronously.

<% ui.includeJavascript("yourmoduleid", "jquery.js") def id = config.id def props = config.properties ?: ["encounterType", "encounterDatetime", "location", "provider"] %> <%= ui.resourceLinks() %> <script> jq = jQuery; jq(function() { jq('#${ id }_button').click(function() { jq.getJSON('${ ui.actionLink("getEncounters") }', { 'start': '${ config.start }', 'end': '${ config.end }', 'properties': [ <%= props.collect { "'${it}'" }.join(",") %> ] }) .success(function(data) { jq('#${ id } > tbody > tr').remove(); var tbody = jq('#${ id } > tbody'); for (index in data) { var item = data[index]; var row = '<tr>'; <% props.each { %> row += '<td>' + item.${ it } + '</td>'; <% } %> row += '</tr>'; tbody.append(row); } }) .error(function(xhr, status, err) { alert('AJAX error ' + err); }) }); }); </script> <input id="${ id }_button" type="button" value="Refresh"/> <table id="${ id }"> <thead> <tr> <% props.each { %> <th>${ ui.message("Encounter." + it) }</th> <% } %> </tr> </thead> <tbody> <% if (encounters) { %> <% encounters.each { enc -> %> <tr> <% props.each { prop -> %> <td><%= ui.format(enc."${prop}") %></td> <% } %> </tr> <% } %> <% } else { %> <tr> <td colspan="4">${ ui.message("general.none") }</td> </tr> <% } %> </tbody> </table>

Since we're going to be updating our table via javascript, we put an "id" attribute on it so we can select it. But since fragments should support being included on a page more than once, we don't want to hardcode that "id". The UI Framework guarantees that your fragment configuration always has an "id" property, by autogenerating one suitable for use as as a DOM id if the fragment request doesn't provide one.

We've also made a few changes to HTML, specifically adding thead and tbody groupings to make it easier to clear and redraw the body, and adding a refresh button.

Most of the code we added to the view is javascript code, done via jQuery. (If you don't know how jQuery works, you need to read a tutorial on it now, since it is out of scope for this one.)

First, we ensure that jQuery is included on the page by doing ui.includeJavascript("yourmoduleid", "jquery.js"). You also need to put a jquery.js file in your module under omod/src/main/webapp/resources/scripts. You can obtain the latest jQuery file at http://jquery.com/download/. After downloading the file be sure to rename it to jquery.js.

We define jq as a shorthand to refer to jQuery, rather than the traditional $. (This is because the dollar sign has special meaning in Groovy templates, and if we used that, we'd always have to escape it with a \, which makes the code quite hard to read. It gets very confusing to skim code when the dollar sign can either mean jQuery or groovy.)

We then make a standard jQuery getJSON call, giving it a URL and some parameters, and attaching success and error callbacks to it. The success callback is straightforward, though it's worth noting that we use the same 'props' configuration object we defined earlier.

The most interesting thing in this fragment is the call to

ui.actionLink("getEncounters")

This generates a link to the "getEncounters" action in the current fragment. (If you wanted to call this action from a different fragment, or directly from a page, you'd want to explicitly say actionLink("yourmoduleid", "encountersToday", "getEncounters").)

Besides that, there are a few other interesting things we do in the JSON call. First, we include the start and end config model properties, using a convenience method to format the dates in yyyy-MM-dd... format. To turn our 'props' Java object into a javascript list, we use a new Groovy collection method, "collect", which applies a closure to all items in the collection, and collects the results into a new list. (In this case we just use that to put single quotes around each property name.) We also use the Groovy collection method "join" which turns a collection into a String, with the given argument as a separator.

Note that unlike in frameworks like JSF and Wicket, fragments in the OpenMRS UI Framework are stateless, and they don't have any conception of how exactly they're configured in a particular browser window. So when the browser asks to load the encounters to be shown, it needs to include any necessary configuration parameters in the query. (In this case those are "start", "end", and "properties".)