OLD- OpenMRS 2.0 Core Application Developers Guide

The UI Framework code is being transitioned to a module. Documentation will move to UI Framework

This page is particularly outdated.

Overview

The OpenMRS 2.0 application framework intends to be

  • Easy and fun to program in
  • Configurable by administrators
  • Flexible

For some background thinking see http://openmrs.atlassian.net/wiki/download/attachments/21534167/OpenMRS-2.0-plenary.pdf?version=1&modificationDate=1285183260000

The framework is built on top of Spring MVC, but most developers will not need to interact with Spring directly. The functionality to produce 90% of functionality is easily accessible through helper functions.

Key concepts you should understand while using the framework are:

  • Recursively composing reusable fragments into higher level fragments, and into pages
  • Flexible reusable content that administrators will reconfigure
  • Rapid development cycles

Pages and Fragments

Most UI frameworks are based around the idea of writing pages, with limited ability to decorate pages, and importing snippets of code described as an afterthought. That's not the right way to think about design for OpenMRS, which is a Medical Record System Framework that will be reconfigured and reused in a wide variety of ways.
You will write pages, and obviously these are important. But pages are merely a mechanism for composing reusable fragments together. Fragments are where the real power lies. Any functionality you write as an OpenMRS developer should be written in a fragment. Preferrably it should be written in the smallest fragment possible, which may then be reused by other fragments.

Expect re-configuration

One of the powerful things about the OpenMRS application is that system administrators will be able to configure exactly how it works for different types of users. To that end, whenever you are writing a fragment, keep in mind that people will take that fragment and reuse it outside of its original context. So, make sure your fragments are flexible, and assume they will be borrowed.

Rapid Development

The framework promotes rapid development in several ways:

  1. in development mode, views and controllers are instantly reloaded whenever you change them.
  2. there are straightforward conventions for controller names and view names
  3. Groovy as a view technology is more powerful and expressive than a JSTL-based technology
    • Consider this code snippet which prints out a list of encounter types sorted by their localized name in the user's locale. If you were using JSTL you would need to do the sorting in the controller:

      <%
      if (encounterTypes) {
      	encounterTypes.collect { ui.format(it) }.sort().each {
      %>
      	${it} <br/>
      <% } %>
      
  4. UI-level domain objects may be defined using JPA Annotations

What if the framework doesn't handle xyz?

If all else fails, you can write code in "regular" Spring MVC. This is discouraged, but the option is always available to you if necessary.

Pages

Documentation for pages is coming, but it's quite similar to fragments.

Overview

Page Controllers

Pages should not contain any functionality of their own, only what's included from fragments. As a result, you rarely need a custom page controller. For example most patient-related pages can use the generic PatientPageController, which provides a minimal model with just patientId and patient properties--fragments will use these and elaborate on them.

Generic Page controllers

Custom Page controllers

Page Views

GSP Page Views

XML Page Views

Fragments

Overview

Fragments are at the heart of the OpenMRS application framework. This is where you should write all UI-layer functionality. Fragments may include other fragments, and they may ask to be decorated by other fragments. Fragments may also be grouped in 'folders', such as 'decorators/' or 'widgets/'.

Fragment Configuration

Fragments are included in pages, or in other fragments. When you include a fragment you can (and usually do) provide it with configuration properties. For example a "locationChooser" widget would take a "formFieldName" configuration property (which might default to "location" if not provided).

Decorators

A decorator is a fragment with a special naming convention, that gets called in a particular way. If a fragment asked to be decorated by "widget" then the framework looks for a fragment named "decorators/widget", and calls that fragment with the rendered content of the decorated fragment as the widget configuration parameter named "content". Decorators may take other configuration parameters, just like any normal fragment.
For example:

<table border="1">
	<tr><th>${ configuration.title }</th></tr>
	<tr><td>${ configuration.content }</td></tr>
</table>

Controllers

The framework looks up page fragment controllers by convention. The controller for fragment 'xyz' is org.openmrs.ui2.fragment.controller.XyzFragmentController. If you have a fragment grouped in a 'folder' the controller should be in that subpackage. For example the fragment 'decorators/xyz' would be org.openmrs.ui2.fragment.controller.decorators.XyzFragmentController.

Controller state

Controllers are not allowed to store references to external objects. This is an unfortunate tradeoff we made because it allows us to reload controller classes instantly in development mode. Note that this mean you cannot use Spring to inject beans into your service. (If you need to access a Spring bean, use the getSpringBean(String beanId, Class ofType) method in PageContext.)
(There may be a way to relax these restrictions, but we haven't had time to explore.)
Also, note that your controller will obviously lose any state it has if you change its code and it is reloaded during development mode.

Base Fragment Controllers

Note that fragment controllers are just POJOs, and their methods (as described below) are regular methods, therefore controllers may extend other controllers. You may take advantage of this by extending an abstract base controllers like MetadataSearchController. See all available base controllers in ??? package. TODO: build an example base controller.

Main controller() method

When a fragment is included in a page, the framework checks its controller for a public method named "controller". If this method exists, it is called. (If no such method exists, then we go straight to the view for the requested fragment.)
The controller method can have a flexible method signature, whose arguments are treated differently based on their type. You will typically use the following parameter types:

  • FragmentConfiguration: the configuration that a parent element provided when including this fragment. (E.g. things like 'title', 'size', or 'items'.)
  • FragmentModel: an empty FragmentModel object which is the model for this fragment. Any entries you add to this map will be exposed in the view.
  • PageModel: the shared page model. Changes you make to this object will be seen by fragments later on in the page, and child fragments your fragment includes in its view.

Main controller methods also support these lesser-used types:

  • FragmentContext: the context for generating this fragment.
  • PageContext: the context for the page this fragment is part of.
  • FragmentRequest: the request for this fragment.
  • PageRequest: the page request from the end user, and the internal page name it was mapped to. (Useful for accessing the session.)
  • HttpServletRequest: the actual http page request. (Useful if you need raw access to parameters passed to the page.)

Controller method return types are also flexible.

  • Usually you will return null, or declare a void return type. In this case the fragment's default view (i.e. the 'xyz' view for the 'xyz' fragment) will be rendered with the model you populated in the controller method. Remember that if no view exists with this name, nothing is displayed, and no error is raised.
  • If you return a String (except one that starts with "redirect:"), then the view with the name you return will be rendered. (For example the controller for "showHivStatus" might return "notPermitted".)
  • If you return a "redirect:pageName", then page rendering will stop (without processing further fragments) and the client will be forwarded to the specified page.
    • You may also achieve the same redirect behavior by throwing a org.openmrs.ui2.page.Redirect.)
  • If you return a FragmentRequest, then instead of rendering a view, the framework will process the new request, and render its output instead. (For example the "editField" fragment might return a request for "editLocationField" to replace it.)

An example:

public class PatientEncountersFragmentController {
	public void controller(PageContext pageContext, Map<String, Object> model) {
		Patient p = (Patient) pageContext.getModel().get("patient"); // get patient from shared page model
		List<Encounter> encounters = Context.getEncounterService().getEncountersByPatient(p);
		model.put("encounters", encounters);
	}
}
Inter-Fragment Communication in Controllers

Fragment controllers have very limited ability to communicate with each other: they may use the shared page model in the PageContext. But note that fragments are rendered in order from top to bottom. So, for example, if you want one fragment to calculate decision-support alerts that will be drawn by a different fragment, they must be included in the page in that order.

Fragment Actions

Your fragment controller may also expose "fragment actions". These are methods that support some common interactions, and are easy to call from a fragment.

Declaration

If you want your 'xyz' fragment to have a 'foo' action, declare a method foo(FragmentActionRequest) in XyzFragmentController. The FragmentActionRequest class gives you convenience methods for getting parameters. (See the API documentation.)

The return value of your action method controls what response the framework sends back to the client.

  • Returning null (or declaring a void return type) is the same as returning a SuccessResult.
  • If you return a SuccessResult, then the framework will redirect to the success URL.
  • If you return a FailureResult, the framework will redirect to the failure URL, and display the errors contained in that result.
  • ObjectResults allow you return the actual value of a POJO (similar to Spring MVC's ResultBody annotation)
    • If you return a plain ObjectResult, the framework will display the result of calling the wrapped object's toString() method. Typically you'll want to use a subclass to indicate how to serialize the wrapped object.
    • If you return a JsonResult, the POJO it wraps will be converted to JSON.
      • TODO: implement XML and make JSON less hacky

An example:

public class PatientIdentifiersFragmentController {
	...
	// TODO this method probably won't work due to hibernate proxies
	public JsonResult getAllIdentifers(FragmentActionRequest) {
		Patient p = request.getRequiredParameter("patientId", Patient.class, "Patient");
		return new JsonResult(p.getActiveIdentifiers());
	}

	public RedirectResult addIdentifier(FragmentActionRequest request) {
		Patient p = request.getRequiredParameter("patientId", Patient.class, "Patient");
		PatientIdentifierType idType = request.getRequiredParameter("identifierType", PatientIdentifierType.class, "Type");
		String identifier = request.getRequiredParameter("identifier", "Identifier");
		Location location = request.getParameter("location", Location.class, "Location");

		if (request.hasErrors()) {
			return new FailureResult(request.getErrors()); // display errors
		} else {
			p.addIdentifier(new PatientIdentifier(identifier, idType, location));
			Context.getPatientService().savePatient(p);
			return new SuccessResult();
		}
	}
}
Usage

To create an HTML form that will submit to a fragment action, do the following:

${ ui.startForm("fragmentName", "actionName", [param1: "val1"]) }
	form elements here
${ ui.endForm() }

To get the actual URL for that fragment action (for example to access an action from a link) use one of:

${ ui.actionLink("fragmentName", "actionName") }
	${ ui.actionLink("fragmentName", "actionName", [param1: "value1", param2: "value2"]) }

To call a fragment action from that fragment itself, you can omit the fragmentName argument and use:

${ ui.startForm("actionName") } ... ${ ui.endForm() }
	${ ui.actionLink("actionName") }
Fixed Parameters

Parameters that you pass to the startForm or actionLink method (as a map) will be passed to the action as part of the link URL. The most common use case for forms is indicating the URLs to return to on success and failure. You might also include the database id of the item you are acting on.
For example if you have a fragment that is configured with a returnUrl parameter, the following would access two different actions to either edit the name of the command object, or retire it, and after doing either, go back to the returnUrl:

${ ui.startForm("editName", [id: command.id, successUrl: configuration.returnUrl]) }
	<input type="text" name="name" value="${command.name}"/>
	<input type="submit" value="${ ui.message("general.save") }" />
${ ui.endForm() }
<a href="${ ui.actionLink("retire", [id: command.id, successUrl: configuration.returnUrl]) }">
	${ ui.message("general.retire") }
</a>

If you don't specify a successUrl or a failureUrl for a fragment action, then they default to being the page you called the action from, which generally has the effect of reloading the current page.

Reloading in development mode

In development mode, controllers will automatically be reloaded whenever you make changes to them. In production mode, this feature will be turned off.

Fragment Views

Most fragments will have a "view" which describes how they are displayed on a page. (Although it's possible for a fragment to be "controller-only"--for example a decision-support fragment might add alerts to the shared page model, to be displayed by another fragment. If the framework cannot find a fragment view for the requested name, nothing is displayed, and no error is thrown.)
There are multiple technologies for writing a fragment view. The file extension of your view file needs to match the technology it uses. Fragment views are looked up by convention. If the view for the 'patient' fragment is written in GSP, it should be /src/main/webapp/WEB-INF/fragments/patient.gsp.
When fragments are grouped into folders, their view source files are places in that subfolder of the base fragments folder. For example the fragment "decorators/widget" might be /src/main/webapp/WEB-INF/fragments/decorators/widget.gsp.
Remember that when you are writing a fragment, you are not writing a whole page. You do not need any < html > tags, and you should include javascript and css resources using the 'ui' helper object.

GSP Fragment Views

Most fragment views will be written in Groovy Server Pages. Use a .gsp file extension for these views. This technology allows you to write HTML, and include snippets of Groovy code. This gives you access to the full power of the Groovy language (and Java, by extension) for conditionals, looping, closures, and in-line declaration of list and map variables. But 90% of what you want to do can be done using the helper functions provided by the "ui" helper object.

(chooser.gsp)
<select name="${ configuration.formFieldName }">
	<option value=""></option>
	<% configuration.options.each { %>
		<option value="${ it.id }"<% if (it == configuration.selected) { %> selected="true"<% } %>>
			${ ui.format(it) }
		</option>
	<% } %>
</select>

Under the hood, these views use groovy.text.SimpleTemplateEngine.

Accessing Fragment Configuration Parameters in GSP Views

The fragment's configuration (which is a Map<String, Object>) is available in the GSP view through the 'configuration' variable. So you would access the title configuration property as 'configuration.title'

Accessing the Fragment Model in GSP Views

All objects in the fragment model (a Map<String, ?>) are available in the view through their names in the map. So you would access the patientId object as 'patientId'.

The 'ui' helper object

The 'ui' helper object gives you access to most of the functionality you need to create fragments. Since these are groovy functions, you can use them together. (For example you could format an EncounterType, use that as an argument to a localized message, and use that message as the page title.)
Some useful methods:

  • message(String code, Object... args): returns a localized message

    ${ ui.message("patientEncounters.title") }
    
  • format(Object o): formats any known object type for printing

    Logged in as ${ ui.format(session.role) } at ${ ui.format(session.location) }
    
  • includeFragment(String fragmentName) and includeFragment(String fragmentName, Map<String, Object> config): to include another fragment

    (chooseLocation.gsp)
    <%= ui.includeFragment("chooser", [
    		selected: configuration.selected,
    		formFieldName: configuration.formFieldName,
    		options: omrs.getLocationService().getAllLocations()
    	]) %>
    
  • decorateWith(String) and decorateWith(String decoratorFragmentName, Map<String, Object> decoratorConfig): tell the framework that this fragment should be wrapped with the specified decorator.

    <% ui.decorateWith("widget", [title: "ID Numbers" ]) %>
    
  • setPageTitle(String): sets the page title.
  • includeJavascript(String file) and includeCss(String file): instruct the page to include the specified file. Will not include the same file twice.
  • requirePrivilege(String privilege): requires that the user has the specified privilege, or else throws them back to the login page.
  • startForm(String actionName) and endForm(): wrap these around an HTML Form that will be submitted to the specified action of the current fragment
  • randomId(String prefix): gets a random value, suitable for use as a DOM id, starting with the given prefix

    <% def id = configuration.id ?: ui.randomId("encounterTable") %>
    

    See the full API.

The 'omrs' helper object

In groovy fragment views, you may access org.openmrs.api.context.Context as "omrs"

${ omrs.getLocationService().getAllLocations() }
Inter-Fragment Communication in Views

You should avoid directly coupling fragments together by having one depend on the existence of the other one in order to function. Coupling makes it harder to maintain code, since relationships between fragments are not explicit, and it makes fragments less reusable. For example it is very bad style to have one fragment directly call a javascript function provided by another fragment.
Instead, the framework provides an event bus mechanism built on PageBus which is encapsulated in publish and subscribe functions in openmrs.js

To publish a message, use the function "publish(topic, payload)". For example:

publish('notify-user', { message: 'The system is restarting' });

To subscribe to messages, use the function "subscribe(topic, callback_function)". The callback function should have the method signature "function(topic, payload)". A simple example:

subscribe('notify-user', function(topic, payload) {
	window.alert(payload.message);
});

You may also subscribe to multiple topics by using * as a wildcard character.

A real-world example:

// the encounterTable fragment does this
<a href="javascript:publish('${ id }.encounterSelected', '${encounter.id}')">Select</a>

// the infobox fragment creates one of these, that supports a showEncounterById function
${ id } = new InfoBox('${ id }');

// the patientEncounters fragment includes both of the above fragments, and connects them:
${ ui.includeFragment("encounterTable", [id: "encTable", encounters: encounters]) }
${ ui.includeFragment("infoBox", [id: "infoBox"]) }

<script>
	subscribe('encTable.encounterSelected', function(topic, msg) {
			id_of_infobox.showEncounterById(msg);
		});
</script>
Not Grails

This is not the same GSP implementation provided by Grails. Be careful while looking at documentation on the web.

XML Fragment Views

If you merely want to compose other fragments together, you can write an XML file. Use a .xml file extension for these views.

<fragment-definition>
	<fragment name="loggedInInfo">
		<decorator>
			widget
		</decorator>
		<decoratorConfiguration>
			<title>Logged in as</title>
		</decoratorConfiguration>
	</fragment>
	<fragment name="chooseLocation"/>
</fragment-definition>

Built-in Fragments

The core OpenMRS application provides several useful fragments that will simplify your life as a developer, and make our applications behave and look much more consistently.

Decorators

  • 'standardPage': almost all pages in the application should use this decorator. They add the standard header, include standard javascript files, show the standard flash message, etc.
  • 'widget': decorates your fragment with JQuery UI's widget decoration. Expects a 'title' config property.
    You would create new decorators in WEB-INF/fragments/decorators.

Widgets

actionButton

TODO: refactor the config properties
Displays a small clickable icon, which can be attached to different actions. It takes a the config properties:

  • data: the data item to use with this button (for 'event' or 'link')
  • action: describes the action to take when the icon is clicked. Takes the subproperties:
    • icon: the filename of the icon to use (in the images/ folder)
    • action: what type of action to perform ("link", "event", or "javascript")
    • (if action="link") url: the url to go to
    • (if action="link") linkParam: parameter name to append to the url
    • (if action="link") linkProperty: this property of 'data' will be appended to the url as a parameter value
    • (if action="event") event: the event published will be (config.actionId).(config.action.event)
    • (if action="event") property: this property of 'data' will be the payload of the event
    • (if action="javascript") javascript: javascript to call when the button is clicked
  • actionId: used when events are published
    Example usage

    refactor config properties first
    
table

A nicely-decorated HTML table that can be populated at page-load time, or via javascript. It supports these config properties:

  • id: the DOM id to give to the table
  • style: CSS style to put on the table
  • rows: a list of objects to display at page-load time
  • columns: a list of column descriptors, which support the properties:
    • heading: string to put in the column header
    • property: if specified, the column will contain this property (either of load-time Java rows or javascript-loaded json objects)
    • actions: list of actions to be rendered as actionButtons
      This widget listens for the "(id).show-data" event.
      Example usage:

      <%= ui.includeFragment("widgets/table",
      		[ id: 'encounter-table',
      		  rows: omrs.getEncounterService().getEncountersByPatient(patient),
      		  columns: [
      		  	[ actions: [
      		  		[ icon: "info_16.png", action: "event", event: "encounterSelected", property: "encounterId" ]
      		  	  ]
      		  	],
      			[ heading: "Date", property: "encounterDatetime" ],
      			[ heading: "Location", property: "location" ],
      			[ heading: "Type", property: "encounterType"]
      		  ]
      		]) %>
      
field

Will display a data entry field or a nicely-formatted value. Supports the config properties:

  • value: the object to be formatted and printed
  • class: the java class of the data entry field to show
  • formFieldName: the name of the field (which will be submitted)
  • visibleFieldId: the DOM id of the user-visible data entry widget. (Some fields may store their data in a hidden input, but you generally can't specify its DOM id.)
  • ? config: extra configuration to be passed to the field implementation (legal values depend on the class)
labeledField

Prints a pretty label and then a field. Supports all the same config properties as the 'field' widget, and also the 'label' property, which is the text to display in the label before this widget.

form

A convenient way to display a pretty form. Supports the config properties:

  • page: if specified, the form will submit to this page.
  • fragment, action: if specified, the form will submit to this fragment and action
  • returnUrl: currently broken
  • fields: a list of elements to include in this form. If an element has a 'fragment' property, that fragment will be included, otherwise it will be passed as config to the labeledField fragment.
  • submitLabel: if this is specified, a submit button will be shown, with this text
    Example usage:

    ${ ui.includeFragment("widgets/form", [
    	fragment: "sendMessage",
    	action: "send",
    	returnUrl: config.returnUrl,
    	fields: [
    		[ label: ui.message("message.from"), value: ui.format(omrs.authenticatedUser) ],
    		[ label: ui.message("message.to"), formFieldName: "to", class: org.openmrs.User.class ],
    		[ label: ui.message("message.subject"), formFieldName: "subject", class: String.class ],
    		[ label: ui.message("message.body"), formFieldName: "body", class: String.class, config: [ type: "textarea" ] ]
    	],
    	submitLabel: ui.message("message.send")
    ])}
    
verticalTabPanel

A vertical tab panel, where each tab is a different fragment. Clicking a tab is actually a new page request for the current page with 'parameter' changed to the newly selected id. Supports the config properties:

  • parameter: the http request parameter that determines which tab is selected. (If this parameter is not present, the first tab is selected.)
  • tabs: a list of objects with the subproperties:
    • id: a unique value for each tab, which will be used as the value of 'parameter'
    • label: text to show on the tab label
    • fragment: the fragment to display if this tab is selected
    • fragmentConfiguration: config parameters for the fragment, if this tab is selected

Fields

UI-Level Domain Objects

You define domain objects by putting JPA annotations on your POJOs. (Link to full documentation.)

@Entity
@Table(uniqueConstraints= {@UniqueConstraint(columnNames={"requestedPage", "role"})})
public class RoleBasedPageRequestMapping {

    private Integer id;
    private Role role;
    ...

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @ManyToOne
    @JoinColumn(name="role", nullable=false)
    public Role getRole() {
        return role;
    }

    public void setRole(Role role) {
    	this.role = role;
    }
    ...
}

These classes will be picked up by Spring's component scanning anywhere in the org.openmrs.ui2, but you should generally put them under the ??? package.
(TODO: how to do liquibase updates)