Inter-Fragment Communication Using the JavaScript Event Bus

The concepts in this section are important, but the code described here will no longer work!
I do suggest reading it, but don't try running the code...

The fact that we are writing reusable page fragments, rather than whole pages, is very powerful, and will lead to lots of flexibility and code reuse across the OpenMRS ecosystem. But it also means we need to introduce a new (more complicated) paradigm for having fragments communicate with each other. For example, we've just built a fragment that shows a list of encounters. The UI Library module includes a fragment that can use ajax to load a preview of an encounter that you select on a page. Clearly, these two fragments are very useful when combined. But at the same time, neither one of them should depend on the other, since they are both perfectly functional alone, and they can both be used with other fragments too. So in order to make our fragments as flexible and reusable as possible, we need to "decouple" them.

The UI Framework includes a mechanism (based on the javascript event bus PageBus) that allows fragments to publish and subscribe to messages, and pass content with those messages.

First, you need to download and install version 1.0 of the UI Library module from the OpenMRS module repository here. (Do get version 1.0 for this tutorial, since we make no promises that the widgets it contains won't change in later module versions. For actual usage, you'll want the latest version.)

To demonstrate that functionality, we are going augment our encountersToday fragment so that it publishes an "encounterSelected" message when you click on one of the listed encounters. Later we'll play around with a couple things we can do with this message.

Edit yourmodule/omod/src/main/webapp/pages/helloWorld.gsp to add one line to the top:

<% ui.decorateWith("standardPage") %>

This decorator is provided by the UI Library module, and it includes standard js and css that the widgets provided by the module depend on.

Now, we can edit the encountersToday.gsp fragment (omitting the refresh button and ajax for simplicity)

<%
    def id = config.id ?: ui.randomId("encountersToday")
    def props = config.properties ?: ["encounterType", "encounterDatetime", "location", "provider"]
    def showSelectButton = config.showSelectButton ?: false
%>

<table id="${ id }" class="decorated">
    <thead>
        <tr>
            <% if (showSelectButton) { %>
                <th></th>
            <% } %>
            <% props.each { %>
                <th>${ ui.message("Encounter." + it) }</th>
            <% } %>
        </tr>
    </thead>
    <tbody>
        <% if (encounters) { %>
            <% encounters.each { enc -> %>
                <tr>
                    <% if (showSelectButton) { %>
                        <td>
                            <a href="javascript:publish('${ id }.encounterSelected', ${ enc.id })">
                                <img src="${ ui.resourceLink("uiframework", "images/info_16.png") }"/>
                            </a>
                        </td>
                    <% } %>
                    <% props.each { prop -> %>
                        <td><%= ui.format(enc."${prop}") %></td>
                    <% } %>
                </tr>
            <% } %>
        <% } else { %>
            <tr>
                <td colspan="4">${ ui.message("general.none") }</td>
            </tr>
        <% } %>
    </tbody>
</table>

We've added a "showSelectButton" config property, which adds an "info" icon at the start of each row. When clicked, these info icons publish encounterSelected messages, giving the id of the encounter selected. Note that the message also includes the (dynamic) id of this fragment. It's possible that multiple fragments on a page will publish an "encounterSelected" message, and we'd like consumers of these fragments to be able to distinguish between them.

Also notice the usage of the "ui.resourceLink(String provider, String path)" method, which allows us to reference content that modules provide in their resources folder. (In this case we are referring to a resource provided by a specific module, which is good style, but if we'd done ui.resourceLink("images/info_16.png"), that would search through all modules until it finds a matching resource.)

We're following good practices here and trying to make our fragment reusable. It can have a select button, but it doesn't require one. In reality we'd want to go further and allow the consumer of the fragment to specify the icon to use, etc.

In order to use this new feature we've added, we need to make some changes to helloWorld.gsp where we define our page. For now, let's just set things up so that when you select an encounter, we open the view encounter page for it.

<% ui.decorateWith("standardPage") %>

${ ui.includeFragment("welcomeMessage") }

${ ui.includeFragment("encountersToday",
        [   start: "2011-02-16",
            end: "2011-02-16 23:59:59.999",
            decorator: "widget",
            decoratorConfig: [title: "Today's Encounters"],
            id: 'encsToday',
            showSelectButton: true
        ]) }

<script>
    jq(document).ready(function() {
        subscribe('encsToday.encounterSelected', function(event, encId) {
            window.location = '${ ui.pageLink("encounter") }?encounterId=' + encId;
        });
    });
</script>

We've done a couple things here. First as we include the encountersToday fragment we enable the select button, and we explicitly give it an id. (Otherwise we wouldn't be able to connect to its events.) Then, via javascript we subscribe to our fragment's encounterSelected message, providing a function that goes to the encounter page with the given id. (Stylistically I like to put javascript that deals with wiring up fragments by subscribing to messages at the very bottom of the page.)

Another thing to notice is the "ui.pageLink(String pageName)" function, which returns the appropriate link to a page, so you don't have to worry about context paths or anything.

To see this in action, try refreshing http://localhost:8080/openmrs/pages/helloWorld.page and click on one of the info icons. You'll get a 404 error, because we have not actually written an encounter page in this tutorial, but you get the idea...

Now, let's do something a bit different. On the helloWorld page, let's also include the "infobox" fragment, and connect it to our encountersToday fragment such that selecting an encounter show a preview of it on our current page, via ajax.

<% ui.decorateWith("standardPage") %>

${ ui.includeFragment("welcomeMessage") }

<table width="100%">
    <tr valign="top">
        <td width="65%">
            ${ ui.includeFragment("encountersToday",
                    [   start: "2011-02-16",
                        end: "2011-02-16 23:59:59.999",
                        properties: ["location", "encounterDatetime"],
                        decorator: "widget",
                        decoratorConfig: [title: "Today's Encounters"],
                        id: 'encsToday',
                        showSelectButton: true
                    ]) }
        </td>
        <td width="35%">
            ${ ui.includeFragment("infobox", [id: "previewBox"]) }
        </td>
    </tr>
</table>

<script>
    jq(function() {
        subscribe('encsToday.encounterSelected', function(topic, encId) {
            previewBox.showEncounter(encId)
        });
    });
</script>

We've included a second fragment (which is provided by the UI Library module), next to our encountersToday one (using a table to do a quick hacky layout). And we've changed the javascript callback for the encounterSelected event to call a method on the infobox fragment (note that that's the id we included it with).

The key point here is that we've written a fragment that we were able to reuse for two different behaviors (go to encounter page, and preview encounter) without changing the fragment itself, but just by wiring it up to different actions. If you follow these patterns while building new application functionality, you will also build up a reusable library of fragments that you can use to assemble future UIs even more quickly. (Imagine writing a page to quickly collect weights and heights by wiring up a find-patient widget, to an obs-table widget with a form widget.) The same pattern will eventually let system administrators compose pages out of fragments that can be wired together using a (not-yet-ready) XML format.