(Prerequisite: [UI Framework Tutorial])
This guide will take you through the process of writing a Patient fragment, while following all [best practices].
A "Patient fragment" is a page fragment that displays data about a patient. While such fragments are typically displayed on a patient dashboard, our best practices will also allow these fragments to be used on pages that aggregate data for multiple patients.
Fragment need to render their data to html on first page load (unless that is very expensive), and also be able to redraw themselves via AJAX, either self-initiated, or when signalled by another fragment on the page.
The example we'll use for this guide is the "Patient Identifiers fragment". We will display a table of PatientIdentifiers, and allow the user to add another (via a short popup form), or delete one (with confirmation) as long as it isn't the last one. This code is already checked into SVN, so you can't literally do these steps yourself, but you could (and should!) use this as a template for building new patient fragments.
Step 1: The controller (quick first pass)
To begin with, we need to write a controller that fetches the patient for us. By default, we want to use the patient from the shared model of the page this fragment is included on, but that should be overridable by a "patient" or "patientId" attribute in the fragment configuration, and if there is not patient available from either of those places, we should throw an exception. All that logic is encapsulated in the FragmentUtil.getPatient method.
Since we're calling our fragment "patientIdentifiers", by convention the controller should be at org.openmrs.ui2.webapp.fragment.controller.PatientIdentifiersFragmentController, and it needs a controller() method.
/*\* * Controller for the PatientIdentifiers fragment. * Displays a table of PatientIdentifiers, and allow the user to add another (via a short popup form), * or delete one (with confirmation) as long as it isn't the last one. \*/ public class PatientIdentifiersFragmentController { /*\* * Controller method when the fragment is included on a page \*/ public void controller(PageModel sharedPageModel, FragmentConfiguration config, FragmentModel model) { model.addAttribute("patient", FragmentUtil.getPatient(sharedPageModel, config)); } }
You'll often need to fetch a bit more patient data than this, but since identifiers are directly on the patient object, a one-line function is sufficient.
Step 2: The view (quick first pass)
In our first pass at writing the view, we'll just display this data in a table. By convention this file should be at /webapp/src/main/webapp/WEB-INF/fragments/patientIdentifiers.gsp.
<% def id = config.id ?: ui.randomId("patientIdentifiers") %> <div id="${ id }"> ${ ui.includeFragment("widgets/table", [ columns: [ [ property: "identifierType", heading: ui.message("PatientIdentifier.type") ], [ property: "identifier", userEntered: true, heading: ui.message("PatientIdentifier.identifier") ], [ property: "location", heading: ui.message("PatientIdentifier.location") ] ], rows: patient.activeIdentifiers, ifNoRowsMessage: ui.message("general.none") ]) } </div>
It would be easy to write the html for the table ourselves, and this would be fine for a quick first pass, but in later steps we're going to want some more sophisticated behavior that we can get for free from the standard OpenMRS "table" widget, so we use that instead. (TODO: the ifNoRowsMessage functionality isn't implemented yet - TRUNK-2173)
Note:
- for localization, we always use the ui.message() function instead of hardcoding user-visible messages in our views.
- for security, we always display user-entered data with appropriate escaping
- in this case we use that's the userEntered setting on the table column, but if we were displaying data ourselves we'd use ui.escapeHtml() or ui.escapeJs()
- for UI consistency, we should always display standard OpenMRS classes with the ui.format() function. (That's taken care of here by the table widget.)
Step 3: Including our patient fragment in standard pages
In the [UI Framework tutorial], you created demonstration page to hold your fragments. Since we are now building real functionality, we want to include our fragment in the real user interface. Since the 2.x application is configurable and customizable, there is no single "patient dashboard" as in OpenMRS 1.x. Adding our fragment to the user interface actually means publishing it in the reference application's library of patient fragments, which are exposed as Extensions. To do this we need to add one line to the org.openmrs.ui2.webapp.extension.CoreExtensionFactory class:
... // PatientFragmentExtensions ret.put("patientFragment.patientIdentifiers", new PatientFragmentExtension("patientIdentifiers.fragment.title", "patientIdentifiers.fragment.description", "patientIdentifiers", null)); ...
We are adding this extension to a Map. The key ("patientFragment.patientIdentifiers") is just a unique name for our extension point, and the value is a (strongly-typed) PatientFragmentExtension. Its constructor arguments are message codes for the label and description, the name of the fragment the extension represents, and (null in this case) fragment configuration attributes.
Once you've made this addition, you will need to rebuild and restart the web application. (This is the only time in the tutorial that you'll need to do this.) Once you've rebuilt, and restarted jetty, you can visit a patient's page, and you will see an "Identifiers" tab.
(On the rare chance that you have manually configured the extension point for patient page tabs, you'll need to manually enable your new fragment.)
Step 4: initial ajax-ification of our widget
Typically anywhere that patient fragments are used, many of them are used together, and they typically all will allow small bits of data entry. As such, we want these widgets to be able to refresh themselves via ajax if other fragments notify them that a specific piece of patient data has changed. We'll start by implementing the ajax refresh, and deal with the message passing a bit later.
The first thing we need to do is write a fragment controller action (a server-side method in our fragment controller) that lets the client fetch the list of identifiers as JSON.
/*\* * Controller for the PatientIdentifiers fragment. * Displays a table of PatientIdentifiers, and allow the user to add another (via a short popup form), * or delete one (with confirmation) as long as it isn't the last one. \*/ public class PatientIdentifiersFragmentController { /*\* * Controller method when the fragment is included on a page \*/ public void controller(PageModel sharedPageModel, FragmentConfiguration config, FragmentModel model) { model.addAttribute("patient", FragmentUtil.getPatient(sharedPageModel, config)); } /*\* * Fragment Action for fetching list of active patient identifiers \*/ public ObjectResult getActiveIdentifiers(UiUtils ui, @RequestParam("patientId") Patient patient) { return ObjectResult.build(ui, patient.getActiveIdentifiers(), "patientIdentifierId", "identifierType", "identifier", "location"); } }
Then we can write a short javascript function that will call this method, and refresh the table. (For rapid development, we're going to write this function directly in the fragment view, but we'll structure it so it can be pulled out into a shared javascript file at a future point.)
<% def id = config.id ?: ui.randomId("patientIdentifiers") %> <script> function refreshPatientIdentifierTable(divId, patientId) { jq('#' + divId + '_table > tbody').empty(); jq.getJSON('${ ui.actionLink("getActiveIdentifiers", [returnFormat: "json"]) }', { patientId: patientId }, function(data) { publish(divId + "_table.show-data", data); }); } </script> <div id="${ id }"> <a href="javascript:refreshPatientIdentifierTable('${ id }', ${ patient.patientId })">test the refresh</a> ${ ui.includeFragment("widgets/table", [ id: id + "_table", columns: [ [ property: "identifierType", heading: ui.message("PatientIdentifier.type") ], [ property: "identifier", userEntered: true, heading: ui.message("PatientIdentifier.identifier") ], [ property: "location", heading: ui.message("PatientIdentifier.location") ] ], rows: patient.activeIdentifiers, ifNoRowsMessage: ui.message("general.none") ]) } </div>
The "table" widget is capable of redrawing itself with new data (using the column definitions it was originally loaded with), so all we have to do is make a standard jQuery getJSON call to our fragment action, passing it the required patientId parameter, and tell the table to redraw itself with the returned data. (We do this by publishing a message we know it's listening for. TODO full documentation of the table fragment.)
In order to test that our code is working, we've also added a temporary button that says "test the refresh". If we reload the page and press the button, we'll see it work.
As an aside, note that here, and everywhere else in this fragment, we need to be careful to use the value of "patient.patientId", and not just a plain "patientId". If our fragment is included on a patient page, there will also be a "patient" model attribute, which will be identical to the patientId property of the "patient" model attribute. But in other contexts this won't be the case. So to keep our fragment flexible, remember to only refer to patient (which we explicitly set in the controller).
Step 5: listening for messages
In reality, we aren't going to have a "refresh" button. Rather we want our fragment to refresh automatically when told that the specific patient data it displays has been updated. (For a list of standardized messages people have already defined, see [the style guide].)
In our patientIdentifiers fragment, we want to redraw ourselves on three different messages:
- (OUR-FRAGMENT-ID).refresh (may be called by generic page decoration)
- "patient/(id).changed" (the generic message that something unspecified about a patient has changed)
- "patient/(id)/identifiers.changed" (the specific message that the patient's identifiers have changed)
The latter two messages promise to include a "patient" javascript object as their message content, but the generic refresh message won't provide us anything.
We add a bit of javascript to listen for these messages:
<% def id = config.id ?: ui.randomId("patientIdentifiers") %> <script> function refreshPatientIdentifierTable(divId, patientId) { jq('#' + divId + '_table > tbody').empty(); jq.getJSON('${ ui.actionLink("getActiveIdentifiers", [returnFormat: "json"]) }', { patientId: patientId }, function(data) { publish(divId + "_table.show-data", data); }); } function refreshPatientIdentifiers${ id }(message, data) { if (data) publish("${ id }\_table.show-data", data.activeIdentifiers); else refreshPatientIdentifierTable('${ id }', ${ patient.patientId }); } subscribe('${ id }.refresh', refreshPatientIdentifiers${ id }); subscribe('patient/${ patient.patientId }.changed', refreshPatientIdentifiers${ id }); subscribe('patient/${ patient.patientId }/identifiers.changed', refreshPatientIdentifiers${ id }); </script> <div id="${ id }"> <a href="javascript:publish('${ id }.refresh')">div id refresh</a> <a href="javascript:patientChanged(${ patient.patientId })">patient changed</a> <a href="javascript:patientChanged(${ patient.patientId }, 'identifiers')">identifiers changed</a> ${ ui.includeFragment("widgets/table", [ id: id + "_table", columns: [ [ property: "identifierType", heading: ui.message("PatientIdentifier.type") ], [ property: "identifier", userEntered: true, heading: ui.message("PatientIdentifier.identifier") ], [ property: "location", heading: ui.message("PatientIdentifier.location") ] ], rows: patient.activeIdentifiers, ifNoRowsMessage: ui.message("general.none") ]) } </div>
We've added three "subscribe" commands, all of which call a new function, that's specific to this instance of our fragment. Note that we've intentionally put as much functionality as we can in the first function (refreshPatientIdentifierTable), which can eventually be refactored out into a cacheable javascript file, while leaving the minimum that must be included directly with the fragment's html in the second function (refreshPatientIdentifiers$
).
We've also changed our temporary test link to test all three caes. The first publishes a refresh message, the next two call a utility function defined in openmrs.js which publishes a patient changed message with the correct payload.) So at this point if you reload the page, and click on the three links, you will see the fragment update correctly. (Note: the 2nd and 3rd links won't currently work, due to a framework bug: TRUNK-2172.)
Step 6: Letting users add an identifier
Now that we're done ajaxifying our fragment, we will implement functionality to let the user add a new identifier.
First, we need to write a fragment action that adds the specified identifier and saves the patient. This is a quick first-pass that will reload the whole page when it's submitted.
/** * Fragment Action for adding a new identifier */ public FragmentActionResult addIdentifier(UiUtils ui, @RequestParam("patientId") Patient patient, @RequestParam("identifierType") PatientIdentifierType idType, @RequestParam("identifier") String identifier, @RequestParam("location") Location location) { patient.addIdentifier(new PatientIdentifier(identifier, idType, location)); Context.getPatientService().savePatient(patient); return new SuccessResult(ui.message("patientIdentifier.added")); }
Next we need a form on the client side that will submit to this fragment action. We're going to take advantage of the "popupForm" widget, which both lets us write this functionality very quicky, and we can count on to fit into the user interface in a consistent way.
<%
def id = config.id ?: ui.randomId("patientIdentifiers")
%>
<script>
function refreshPatientIdentifierTable(divId, patientId) {
jq('#' + divId + '_table > tbody').empty();
jq.getJSON('$
',
,
function(data)
);
}
function refreshPatientIdentifiers$
(message, data) {
if (data)
publish("$
_table.show-data", data.activeIdentifiers);
else
refreshPatientIdentifierTable('$
', $
);
}
subscribe('$
.refresh', refreshPatientIdentifiers$
);
subscribe('patient/$
.changed', refreshPatientIdentifiers$
);
subscribe('patient/$
/identifiers.changed', refreshPatientIdentifiers$
);
</script>
<div id="$
">
$
$
</div>
We've added the "popupForm" fragment, which will give us: * a button (labeled with "buttonLabel") that pops up a form in a dialog (titled by "popupTitle") * the form submits to the given "action" of the given "fragment" * the form will have submit and cancel buttons labeled with "submitLabel" and "cancelLabel" * the form will have four fieldsone hidden input, and three visible fields, with the specified labels ** the first is a hidden input, with the given name and value ** the next three will actually delegate to the "labeledField" widget for the given class, with the given label and form field name *** TODO document the field widget Also, we've removed our temporary testing links. If you reload the page, you'll now see an "Add" button below the table, which pops up a modal dialog that lets you add an identifier, and reloads the page. (In a later step, we'll fix this up so it refreshes things via our ajax functions.) h2. Step 7: Letting users delete an identifier Now that users can add identifiers, we also want to let them delete identifiers. In first pass we'll add this to the page such that deleting an identifier reloads the page, and we'll ajaxify the interaction in a later step. First, we need a fragment action for deleting identifiers. This is straightforward: {code:title=Delete Identifier fragment action, first pass} /** * Fragment Action for deleting an existing identifier */ public FragmentActionResult deleteIdentifier(UiUtils ui, @RequestParam("patientIdentifierId") Integer id) { PatientService ps = Context.getPatientService(); PatientIdentifier pid = ps.getPatientIdentifier(id); ps.voidPatientIdentifier(pid, "user interface"); return new SuccessResult(ui.message("PatientIdentifier.deleted")); }
Adding this to the view is a bit more complicated. The "table" widget we are using supports having columns with lists of "actions", so we'll take advantage of that:
<%
def id = config.id ?: ui.randomId("patientIdentifiers")
%>
<script>
function refreshPatientIdentifierTable(divId, patientId) {
jq('#' + divId + '_table > tbody').empty();
jq.getJSON('$
',
,
function(data)
);
}
function refreshPatientIdentifiers$
(message, data) {
if (data)
publish("$
_table.show-data", data.activeIdentifiers);
else
refreshPatientIdentifierTable('$
', $
);
}
subscribe('$
.refresh', refreshPatientIdentifiers$
);
subscribe('patient/$
.changed', refreshPatientIdentifiers$
);
subscribe('patient/$
/identifiers.changed', refreshPatientIdentifiers$
);
subscribe('$
_table.delete-button-clicked', function(message, data) {
if (prettyConfirm('$
')) {
jq.post('$
',
,
function(data)
)
.error(function()
)
}
});
</script>
<div id="$
">
$
$
</div>
(Note: the details of this may change when the "table" widget is refactored - TRUNK-2060) We've added two things: an action as one of the table columns, and an event subscription that listens for the button being clicked. (It might seem more natural if the action in the table row called a plain javascript function, but the event mechanism makes it easier for the table widget to handle the same action on the inital page load, and when the table is populated via ajax.) The newly-added last column of the table is a list of actions (containing just one action). Visually, we define an icon (TODO document this) and a tooltip. Logically, we define an event to post, an eventPrefix (used so that this fragment can be included on a page multiple times), and a property, whose value for the clicked row will be published as additional data in the message. In the newly-added event subscription, we listen for (FRAGMENT-ID)_table.delete-button-clicked, namely the eventPrefix (dot) the event. The callback function we register with the subscription counts on being passed the patientIdentifierId (i.e. the "property" on the action) as extra data. We ask the user to confirm their action using the openmrsConfirm function. (Currently this just calls the standard Javascript confirm function, but by using our own OpenMRS method, we'll be able to make the look and feel prettier in the future, by changing code in just one place.) Assuming the users confirms the deletion, we do a standard jQuery post to our new deleteIdentifier action, passing the relevant id as data, and reloading the page on success. (This is a placeholder--we'll ajaxify shortly.) Finally, we define a function to be called on error. It is good practice to handle errors from every ajax call you make--displaying anything at all (even something not particularly meaningful to the end user) is better than a silent error. (If you were paying very close attention you'll have noticed that we passed "id" as the eventPrefix in the fragment configuration, but the message prefix we actually subscribe to is the id of the _table_ fragment we include, and not the id of our own fragment. TODO describe why this happens.) If you reload your page, you'll now be able to add and remove identifiers. h2. Step 8: Ajaxifying the add and remove identifier interactions Changing the add and remove actions so that the work via AJAX is actually quite easy, since we can let the UI framework do most of the hard work for us. The first things we need to do is change our addIdentifier and deleteIdentifier fragment actions to have them return ObjectResults. All ajaxified patient fragments are expected to publish a patient changed event, with a standard simplified javascript representation of the Patient object. (This will allow multiple page fragments to redraw themselves when a piece of patient data is changed, without needing to make their own ajax calls to the server, improving OpenMRS's functionality over slow network connections.) We use a standard utility method to produce the json-ready patient object. (As you build other patient fragments, you will likely need to tweak that standardized method by adding more properties.) {code:title=Controller, almost final} /** * Controller for the PatientIdentifiers fragment. * Displays a table of PatientIdentifiers, and allow the user to add another (via a short popup form), * or delete one (with confirmation) as long as it isn't the last one. */ public class PatientIdentifiersFragmentController { /** * Controller method when the fragment is included on a page */ public void controller(PageModel sharedPageModel, FragmentConfiguration config, FragmentModel model) { model.addAttribute("patient", FragmentUtil.getPatient(sharedPageModel, config)); } /** * Fragment Action for fetching list of active patient identifiers */ public ObjectResult getActiveIdentifiers(UiUtils ui, @RequestParam("patientId") Patient patient) { return ObjectResult.build(ui, patient.getActiveIdentifiers(), "patientIdentifierId", "identifierType", "identifier", "location"); } /** * Fragment Action for adding a new identifier */ public FragmentActionResult addIdentifier(UiUtils ui, @RequestParam("patientId") Patient patient, @RequestParam("identifierType") PatientIdentifierType idType, @RequestParam("identifier") String identifier, @RequestParam("location") Location location) { patient.addIdentifier(new PatientIdentifier(identifier, idType, location)); Context.getPatientService().savePatient(patient); return FragmentUtil.standardPatientObject(ui, patient); } /** * Fragment Action for deleting an existing identifier */ public FragmentActionResult deleteIdentifier(UiUtils ui, @RequestParam("patientIdentifierId") Integer id) { PatientService ps = Context.getPatientService(); PatientIdentifier pid = ps.getPatientIdentifier(id); ps.voidPatientIdentifier(pid, "user interface"); return FragmentUtil.standardPatientObject(ui, pid.getPatient()); } }
The only change we've made is to return a ObjectResult representing a standard patient, instead of a SuccessResult.
Next, we need to change the add and delete actions in the view to support these new object return values (as JSON):
<% def id = config.id ?: ui.randomId("patientIdentifiers") %> <script> function refreshPatientIdentifierTable(divId, patientId) { jq('#' + divId + '_table > tbody').empty(); jq.getJSON('${ ui.actionLink("getActiveIdentifiers", [returnFormat: "json"]) }', { patientId: patientId }, function(data) { publish(divId + "_table.show-data", data); }); } function refreshPatientIdentifiers${ id }(message, data) { if (data) publish("${ id }\_table.show-data", data.activeIdentifiers); else refreshPatientIdentifierTable('${ id }', ${ patient.patientId }); } subscribe('${ id }.refresh', refreshPatientIdentifiers${ id }); subscribe('patient/${ patient.patientId }.changed', refreshPatientIdentifiers${ id }); subscribe('patient/${ patient.patientId }/identifiers.changed', refreshPatientIdentifiers${ id }); subscribe('${ id }\_table.delete-button-clicked', function(message, data) { if (openmrsConfirm('${ ui.message("general.confirm") }')) { jq.post('${ ui.actionLink("deleteIdentifier") }', { returnFormat: 'json', patientIdentifierId: data }, function(data) { flashSuccess('${ ui.escapeJs(ui.message("PatientIdentifier.deleted")) }'); publish('patient/${ patient.patientId }/identifiers.changed', data); }, 'json') .error(function() { flashError("Programmer error: delete identifier failed"); }) } }); </script> <div id="${ id }"> ${ ui.includeFragment("widgets/table", [ id: id + "_table", columns: [ [ property: "identifierType", heading: ui.message("PatientIdentifier.identifierType") ], [ property: "identifier", userEntered: true, heading: ui.message("PatientIdentifier.identifier") ], [ property: "location", heading: ui.message("PatientIdentifier.location") ], [ actions: [ [ action: "event", icon: "delete24.png", tooltip: ui.message("PatientIdentifier.delete"), event: "delete-button-clicked", eventPrefix: id, property: "patientIdentifierId" ] ] ] ], rows: patient.activeIdentifiers, ifNoRowsMessage: ui.message("general.none") ]) } ${ ui.includeFragment("widgets/popupForm", [ id: id + "_add", buttonLabel: ui.message("general.add"), popupTitle: ui.message("PatientIdentifier.add"), fragment: "patientIdentifiers", action: "addIdentifier", submitLabel: ui.message("general.save"), cancelLabel: ui.message("general.cancel"), fields: [ [ hiddenInputName: "patientId", value: patient.patientId ], [ label: ui.message("PatientIdentifier.identifierType"), formFieldName: "identifierType", class: org.openmrs.PatientIdentifierType ], [ label: ui.message("PatientIdentifier.identifier"), formFieldName: "identifier", class: java.lang.String ], [ label: ui.message("PatientIdentifier.location"), formFieldName: "location", class: org.openmrs.Location ] ], successEvent: "patient/" + patient.patientId + "/identifiers.changed" ]) } </div>
Handling the Add Identifier action is easy: we just need to add a "successEvent" configuration attribute to the popupForm widget. This automatically tells the form widget it should post data via AJAX, instead of a regular form submission, and publish the specified event, with the post's returned JSON value as a payload.
Handling the Remove Identifier action is easy as well. We had already defined a function callback for the delete-button-clicked event that did an AJAX post via jQuery. Now we just modify its success callback function slightly (to show a success message, and publish the patient changed message) and specify the 'json' return type (otherwise jQuery would pass a String to our success function instead of a javascript object).
Once you've made those changes, you should be able to reload the page, and add and remove multiple identifiers without having to do a full page reload.
Note: you'll probably notice a few framework bugs as you play around with the add and remove, specifically:
- TRUNK-2180 - form widget needs to clear itself after a successful ajax submission
- TRUNK-2179 - table widget needs to support "event" actions when loading data via ajax
Step 9: Additional validation
TODO: don't let the user delete the last active identifier