Message-ID: <293894911.1558.1495578715621.JavaMail.confluence@gw81> Subject: Exported From Confluence MIME-Version: 1.0 Content-Type: multipart/related; boundary="----=_Part_1557_547142195.1495578715618" ------=_Part_1557_547142195.1495578715618 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: quoted-printable Content-Location: file:///C:/exported.html 2.x core Patient Fragment step by step tutorial

2.x core Patient Fragment step by step tutorial

=20 =20
=20

The UI Framework code is being transitioned to a module. Documentation w= ill move to =EF=BB=BFUI Framewo= rk.

=20
=20

(Prerequisite: UI Framework Step By Step Tutorial)

=20

This guide will take you through the process of writing a Patient fragme= nt, while following all best practices.

=20

A "Patient fragment" is (obviously) a page fragment that displ= ays data about a patient. While such fragments are typically displayed on a= patient dashboard, our best practices will also allow these fragments to b= e used on pages that aggregate data for multiple patients.

=20

Fragment need to render their data to html on first page load (unless th= at is very expensive), and also be able to redraw themselves via AJAX, eith= er self-initiated, or when signalled by another fragment on the page.

= =20

The example we'll use for this guide is the "Patient Identifiers fr= agment". We will display a table of PatientIdentifiers, and allow the = user to add another (via a short popup form), or delete one (with confirmat= ion) as long as it isn't the last one. This code is already checked into SV= N, so you can't literally do these steps yourself, but you could (and shoul= d!) use this as a template for building new patient fragments.

=20

Step 1: The controller (quick first pass)

=20

To begin with, we need to write a controller that fetches the patient fo= r us. By default, we want to use the patient from the shared model of the p= age this fragment is included on, but that should be overridable by a "= ;patient" or "patientId" attribute in the fragment configura= tion, and if there is not patient available from either of those places, we= should throw an exception. All that logic is encapsulated in the FragmentU= til.getPatient method.

=20

Since we're calling our fragment "patientIdentifiers", by conv= ention the controller should be at org.openmrs.ui2.webapp.fragment.controll= er.PatientIdentifiersFragmentController, and it needs a controller() method= .

=20
controller, first pass
=20
/**
 * Controller for the PatientIdentifiers fragment.
 * Displays a table of PatientIdentifiers, and allow the user to add anothe=
r (via a short popup form),
 * or delete one (with confirmation) as long as it isn't the last one.
 */
public class PatientIdentifiersFragmentController {

=09/**
=09 * Controller method when the fragment is included on a page
=09 */
=09public void controller(PageModel sharedPageModel, FragmentConfiguration =
config, FragmentModel model) {
=09=09model.addAttribute("patient", FragmentUtil.getPatient(share=
dPageModel, config));
=09}

}
=20
=20

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 suff= icient.

=20

Step 2: The view (quick first pass)

=20

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-IN= F/fragments/patientIdentifiers.gsp.

=20
view, first pass
=20
<%
    def id =3D config.id ?: ui.randomId("patientIdentifiers")
%>

<div id=3D"${ id }">
    ${ ui.includeFragment("widgets/table", [
            columns: [
                [ property: "identifierType", heading: ui.message=
("PatientIdentifier.type") ],
                [ property: "identifier", userEntered: true, head=
ing: ui.message("PatientIdentifier.identifier") ],
                [ property: "location", heading: ui.message("=
;PatientIdentifier.location") ]
            ],
            rows: patient.activeIdentifiers,
            ifNoRowsMessage: ui.message("general.none")
        ]) }
</div>
=20
=20

It would be easy to write the html for the table ourselves, and this wou= ld be fine for a quick first pass, but in later steps we're going to want s= ome more sophisticated behavior that we can get for free from the standard = OpenMRS "table" widget, so we use that instead.

=20

Note:

=20 =20

Step 3: Including our patient fragment in stan= dard pages

=20

In the UI F= ramework tutorial, you created demonstration page to hold your fragment= s. Since we are now building real functionality, we want to include our fra= gment in the real user interface. Since the 2.x application is configurable= and customizable, there is no single "patient dashboard" as in O= penMRS 1.x. Adding our fragment to the user interface actually means publis= hing 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.op= enmrs.ui2.webapp.extension.CoreExtensionFactory class:

=20
Publishing our fragment
=20
...
// PatientFragmentExtensions
ret.put("patientFragment.patientIdentifiers", new PatientFragment=
Extension("patientIdentifiers.fragment.title",
        "patientIdentifiers.fragment.description", "patientI=
dentifiers", null));
...
=20
=20

We are adding this extension to a Map. The key ("patientFragment.pa= tientIdentifiers") is just a unique name for our extension point, and = the value is a (strongly-typed) PatientFragmentExtension. Its constructor a= rguments are message codes for the label and description, the name of the f= ragment the extension represents, and (null in this case) fragment configur= ation attributes.

=20

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 t= o do this.) Once you've rebuilt, and restarted jetty, you can visit a patie= nt's page, and you will see an "Identifiers" tab.

=20

(On the rare chance that you have manually configured the extension poin= t for patient page tabs, you'll need to manually enable your new fragment.)=

=20

Step 4: initial ajax-ification of our widget

=20

Typically anywhere that patient fragments are used, many of them are use= d 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 ot= her 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 p= assing a bit later.

=20

The first thing we need to do is write a fragment controller action (a s= erver-side method in our fragment controller) that lets the client fetch th= e list of identifiers as JSON.

=20
add a fragment action for fetching patient's active identifiers
=20
/**
 * Controller for the PatientIdentifiers fragment.
 * Displays a table of PatientIdentifiers, and allow the user to add anothe=
r (via a short popup form),
 * or delete one (with confirmation) as long as it isn't the last one.
 */
public class PatientIdentifiersFragmentController {

=09/**
=09 * Controller method when the fragment is included on a page
=09 */
=09public void controller(PageModel sharedPageModel, FragmentConfiguration =
config, FragmentModel model) {
=09=09model.addAttribute("patient", FragmentUtil.getPatient(share=
dPageModel, config));
=09}

=09/**
=09 * Fragment Action for fetching list of active patient identifiers
=09 */
=09public Object getActiveIdentifiers(UiUtils ui,
=09                                   @RequestParam("patientId") =
Patient patient) {
=09=09return SimpleObject.fromCollection(patient.getActiveIdentifiers(), ui=
,
=09=09=09"patientIdentifierId", "identifierType", "=
;identifier", "location");
=09}
}
=20
=20

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.)

=20
refresh function in the view
=20
<%
    def id =3D config.id ?: ui.randomId("patientIdentifiers")
%>
<script>
    function refreshPatientIdentifierTable(divId, patientId) {
        jq('#' + divId + '_table > tbody').empty();
        jq.getJSON('${ ui.actionLink("getActiveIdentifiers", [ret=
urnFormat: "json"]) }', { patientId: patientId },
        function(data) {
            publish(divId + "_table.show-data", data);
        });
    }
</script>

<div id=3D"${ id }">
    <a href=3D"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, head=
ing: ui.message("PatientIdentifier.identifier") ],
                [ property: "location", heading: ui.message("=
;PatientIdentifier.location") ]
            ],
            rows: patient.activeIdentifiers,
            ifNoRowsMessage: ui.message("general.none")
        ]) }
</div>
=20
=20

The "table" widget is capable of redrawing itself with new dat= a (using the column definitions it was originally loaded with), so all we h= ave to do is make a standard jQuery getJSON call to our fragment action, pa= ssing it the required patientId parameter, and tell the table to redraw its= elf with the returned data. (We do this by publishing a message we know it'= s listening for. TODO full documentation of the table fragment.)

=20

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 pr= ess the button, we'll see it work.

=20

As an aside, note that here, and everywhere else in this fragment, we ne= ed to be careful to use the value of "patient.patientId", and not= just a plain "patientId". If our fragment is included on a patie= nt page, there will also be a "patient" model attribute, which wi= ll 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 fra= gment flexible, remember to only refer to patient (which we explicitly set = in the controller).

=20

Step 5: listening for messages

=20

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 mess= ages people have already defined, see the style guide.)

=20

In our patientIdentifiers fragment, we want to redraw ourselves on three= different messages:

=20 =20

We add a bit of javascript to listen for these messages:

=20
Refresh on certain messages
=20
<%
    def id =3D config.id ?: ui.randomId("patientIdentifiers")
%>
<script>
    function refreshPatientIdentifierTable(divId, patientId) {
        jq('#' + divId + '_table > tbody').empty();
        jq.getJSON('${ ui.actionLink("getActiveIdentifiers", [ret=
urnFormat: "json"]) }', { patientId: patientId },
        function(data) {
            publish(divId + "_table.show-data", data);
        });
    }

    function refreshPatientIdentifiers${ id }(message, data) {
        if (data && data.activeIdentifiers)
            publish("${ id }_table.show-data", data.activeIdentif=
iers);
        else
            refreshPatientIdentifierTable('${ id }', ${ patient.patientId }=
);
    }

    subscribe('${ id }.refresh', refreshPatientIdentifiers${ id });
    subscribe('patient/${ patient.patientId }.changed', refreshPatientIdent=
ifiers${ id });
    subscribe('patient/${ patient.patientId }/identifiers.changed', refresh=
PatientIdentifiers${ id });
</script>

<div id=3D"${ id }">
    <a href=3D"javascript:publish('${ id }.refresh')">div i=
d refresh</a>
    <a href=3D"javascript:patientChanged(${ patient.patientId })&qu=
ot;>patient changed</a>
    <a href=3D"javascript:patientChanged(${ patient.patientId }, 'i=
dentifiers')">identifiers changed</a>

    ${ ui.includeFragment("widgets/table", [
            id: id + "_table",
            columns: [
                [ property: "identifierType", heading: ui.message=
("PatientIdentifier.type") ],
                [ property: "identifier", userEntered: true, head=
ing: ui.message("PatientIdentifier.identifier") ],
                [ property: "location", heading: ui.message("=
;PatientIdentifier.location") ]
            ],
            rows: patient.activeIdentifiers,
            ifNoRowsMessage: ui.message("general.none")
        ]) }
</div>
=20
=20

We've added three "subscribe" commands, all of which call a ne= w 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 int= o a cacheable javascript file, while leaving the minimum that must be inclu= ded directly with the fragment's html in the second function (refreshPatien= tIdentifiers + id).

=20

We've also changed our temporary test link to test all three cases. The = first publishes a refresh message, the next two call a utility function def= ined in openmrs.js which publishes a patient changed message with the corre= ct payload.) So at this point if you reload the page, and click on the thre= e links, you will see the fragment update correctly.

=20

Step 6: Letting users add an identifier

=20

Now that we're done ajaxifying our fragment, we will implement functiona= lity to let the user add a new identifier.

=20

First, we need to write a fragment action that adds the specified identi= fier and saves the patient. This is a quick first-pass that will reload the= whole page when it's submitted.

=20
Add Identifier fragment action, first pass
=20
/**
=09 * Fragment Action for adding a new identifier
=09 */
=09public FragmentActionResult addIdentifier(UiUtils ui,
=09                                          @RequestParam("patientId&=
quot;) Patient patient,
=09                                          @RequestParam("identifier=
Type") PatientIdentifierType idType,
=09                                          @RequestParam("identifier=
") String identifier,
=09                                          @RequestParam("location&q=
uot;) Location location) {
=09=09patient.addIdentifier(new PatientIdentifier(identifier, idType, locat=
ion));
=09=09Context.getPatientService().savePatient(patient);
=09=09return new SuccessResult(ui.message("patientIdentifier.added&quo=
t;));
=09}
=20
=20

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.

=20
fragment view, now with add button
=20
<%
    def id =3D config.id ?: ui.randomId("patientIdentifiers")
%>
<script>
    function refreshPatientIdentifierTable(divId, patientId) {
        jq('#' + divId + '_table > tbody').empty();
        jq.getJSON('${ ui.actionLink("getActiveIdentifiers", [ret=
urnFormat: "json"]) }', { patientId: patientId },
        function(data) {
            publish(divId + "_table.show-data", data);
        });
    }

    function refreshPatientIdentifiers${ id }(message, data) {
        if (data && data.activeIdentifiers)
            publish("${ id }_table.show-data", data.activeIdentif=
iers);
        else
            refreshPatientIdentifierTable('${ id }', ${ patient.patientId }=
);
    }

    subscribe('${ id }.refresh', refreshPatientIdentifiers${ id });
    subscribe('patient/${ patient.patientId }.changed', refreshPatientIdent=
ifiers${ id });
    subscribe('patient/${ patient.patientId }/identifiers.changed', refresh=
PatientIdentifiers${ id });
</script>

<div id=3D"${ id }">
    ${ ui.includeFragment("widgets/table", [
            id: id + "_table",
            columns: [
                [ property: "identifierType", heading: ui.message=
("PatientIdentifier.identifierType") ],
                [ property: "identifier", userEntered: true, head=
ing: ui.message("PatientIdentifier.identifier") ],
                [ property: "location", heading: ui.message("=
;PatientIdentifier.location") ]
            ],
            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.pa=
tientId ],
                [ label: ui.message("PatientIdentifier.identifierType&=
quot;), formFieldName: "identifierType", class: org.openmrs.Patie=
ntIdentifierType ],
                [ label: ui.message("PatientIdentifier.identifier"=
;), formFieldName: "identifier", class: java.lang.String ],
                [ label: ui.message("PatientIdentifier.location")=
, formFieldName: "location", class: org.openmrs.Location ]
            ]
        ]) }
</div>
=20
=20

We've added the "popupForm" fragment, which will give us:

= =20 =20

Also, we've removed our temporary testing links.

=20

If you reload the page, you'll now see an "Add" button below t= he 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 thin= gs via our ajax functions.)

=20

Step 7: Letting users delete an identifier

=20

Now that users can add identifiers, we also want to let them delete iden= tifiers. In first pass we'll add this to the page such that deleting an ide= ntifier reloads the page, and we'll ajaxify the interaction in a later step= .

=20

First, we need a fragment action for deleting identifiers. This is strai= ghtforward:

=20
Delete Identifier fragment action, first pass
=20
/**
=09 * Fragment Action for deleting an existing identifier
=09 */
=09public FragmentActionResult deleteIdentifier(UiUtils ui,
=09                                             @RequestParam("patient=
IdentifierId") Integer id) {
=09=09PatientService ps =3D Context.getPatientService();
=09=09PatientIdentifier pid =3D ps.getPatientIdentifier(id);
=09=09ps.voidPatientIdentifier(pid, "user interface");
=09=09return new SuccessResult(ui.message("PatientIdentifier.deleted&q=
uot;));
=09}
=20
=20

Adding this to the view is a bit more complicated. The "table"= widget we are using supports having columns with lists of "actions&qu= ot;, so we'll take advantage of that:

=20
fragment view, now with delete button
=20
<%
    def id =3D config.id ?: ui.randomId("patientIdentifiers")
%>
<script>
    function refreshPatientIdentifierTable(divId, patientId) {
        jq('#' + divId + '_table > tbody').empty();
        jq.getJSON('${ ui.actionLink("getActiveIdentifiers", [ret=
urnFormat: "json"]) }', { patientId: patientId },
        function(data) {
            publish(divId + "_table.show-data", data);
        });
    }

    function refreshPatientIdentifiers${ id }(message, data) {
        if (data && data.activeIdentifiers)
            publish("${ id }_table.show-data", data.activeIdentif=
iers);
        else
            refreshPatientIdentifierTable('${ id }', ${ patient.patientId }=
);
    }

    subscribe('${ id }.refresh', refreshPatientIdentifiers${ id });
    subscribe('patient/${ patient.patientId }.changed', refreshPatientIdent=
ifiers${ id });
    subscribe('patient/${ patient.patientId }/identifiers.changed', refresh=
PatientIdentifiers${ id });

    subscribe('${ id }_table.delete-button-clicked', function(message, data=
) {
        if (openmrsConfirm('${ ui.message("general.confirm") }'))=
 {
            jq.post('${ ui.actionLink("deleteIdentifier") }', { r=
eturnFormat: 'json', patientIdentifierId: data },
            function(data) {
                location.reload(true);
            })
            .error(function() {
                notifyError("Programmer error: delete identifier faile=
d");
            })
        }
    });
</script>

<div id=3D"${ id }">
    ${ ui.includeFragment("widgets/table", [
            id: id + "_table",
            columns: [
                [ property: "identifierType", heading: ui.message=
("PatientIdentifier.identifierType") ],
                [ property: "identifier", userEntered: true, head=
ing: ui.message("PatientIdentifier.identifier") ],
                [ property: "location", heading: ui.message("=
;PatientIdentifier.location") ],
                [ actions: [
                    [ action: "event",
                        icon: "delete24.png",
                        tooltip: ui.message("PatientIdentifier.delete&=
quot;),
                        event: id + ".delete-button-clicked",
                        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.pa=
tientId ],
                [ label: ui.message("PatientIdentifier.identifierType&=
quot;), formFieldName: "identifierType", class: org.openmrs.Patie=
ntIdentifierType ],
                [ label: ui.message("PatientIdentifier.identifier"=
;), formFieldName: "identifier", class: java.lang.String ],
                [ label: ui.message("PatientIdentifier.location")=
, formFieldName: "location", class: org.openmrs.Location ]
            ]
        ]) }
</div>
=20
=20

(Note: the details of this may change when the "table" widget = is refactored - TRUNK-2060)

=20

(Note that I had to change 'event: id + ".delete-button-clicked&quo= t;' to 'event: id + "_table.delete-button-clicked"' to get this t= o work - Mark)

=20

We've added two things: an action as one of the table columns, and an ev= ent subscription that listens for the button being clicked. (It might seem = more natural if the action in the table row called a plain javascript funct= ion, 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 v= ia ajax.)

=20

The newly-added last column of the table is a list of actions (containin= g just one action). Visually, we define an icon (TODO document this) and a = tooltip. Logically, we define an event to post (including the id of this fr= agment, so that it is safe to include on a page multiple times), and a prop= erty, whose value for the clicked row will be published as additional data = in the message.

=20

In the newly-added event subscription, we listen for (FRAGMENT-ID).delet= e-button-clicked, namely the event we defined in the action. The callback f= unction we register with the subscription counts on being passed the patien= tIdentifierId (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 pret= tier 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 deleteIdent= ifier action, passing the relevant id as data, and reloading the page on su= ccess. (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 pa= rticularly meaningful to the end user) is better than a silent error.

= =20

If you reload your page, you'll now be able to add and remove identifier= s.

=20

Step 8: Ajaxifying the add and remove ide= ntifier interactions

=20

Changing the add and remove actions so that the work via AJAX is actuall= y quite easy, since we can let the UI framework do most of the hard work fo= r us. The first things we need to do is change our addIdentifier and delete= Identifier fragment actions to have them return Objects. All ajaxified pati= ent fragments are expected to publish a patient changed event, with a javas= cript representation of the Patient object in a standard format. We are goi= ng to use a standard utility method to produce the json-ready patient objec= t. (This utility method may not contain all the properties you want. You ma= y want to tweak the method to return a patient object with more properties,= but don't overdo it. You may also construct a result with the utility meth= ods in the SimpleObject class.)

=20
Controller, almost final
=20
/**
 * Controller for the PatientIdentifiers fragment.
 * Displays a table of PatientIdentifiers, and allow the user to add anothe=
r (via a short popup form),
 * or delete one (with confirmation) as long as it isn't the last one.
 */
public class PatientIdentifiersFragmentController {

=09/**
=09 * Controller method when the fragment is included on a page
=09 */
=09public void controller(PageModel sharedPageModel, FragmentConfiguration =
config, FragmentModel model) {
=09=09model.addAttribute("patient", FragmentUtil.getPatient(share=
dPageModel, config));
=09}

=09/**
=09 * Fragment Action for fetching list of active patient identifiers
=09 */
=09public Object getActiveIdentifiers(UiUtils ui, @RequestParam("patie=
ntId") Patient patient) {
=09=09return SimpleObject.fromCollection(patient.getActiveIdentifiers(), ui=
, "patientIdentifierId", "identifierType", "identi=
fier",
=09=09    "location");
=09}

=09/**
=09 * Fragment Action for adding a new identifier
=09 */
=09public Object addIdentifier(UiUtils ui, @RequestParam("patientId&qu=
ot;) Patient patient,
=09        @RequestParam("identifierType") PatientIdentifierType =
idType, @RequestParam("identifier") String identifier,
=09        @RequestParam("location") Location location) {
=09=09patient.addIdentifier(new PatientIdentifier(identifier, idType, locat=
ion));
=09=09Context.getPatientService().savePatient(patient);
=09=09return FragmentUtil.standardPatientObject(ui, patient);
=09}

=09/**
=09 * Fragment Action for deleting an existing identifier
=09 */
=09public Object deleteIdentifier(UiUtils ui, @RequestParam("patientId=
entifierId") Integer id) {
=09=09PatientService ps =3D Context.getPatientService();
=09=09PatientIdentifier pid =3D ps.getPatientIdentifier(id);
=09=09ps.voidPatientIdentifier(pid, "user interface");
=09=09return FragmentUtil.standardPatientObject(ui, pid.getPatient());
=09}

}
=20
=20

The only change we've made is to return an Object representing a standar= d patient, instead of a SuccessResult.

=20

Next, we need to change the add and delete actions in the view to suppor= t these new object return values (as JSON):

=20
View, almost final
=20
<%
    def id =3D config.id ?: ui.randomId("patientIdentifiers")
%>
<script>
    function refreshPatientIdentifierTable(divId, patientId) {
        jq('#' + divId + '_table > tbody').empty();
        jq.getJSON('${ ui.actionLink("getActiveIdentifiers", [ret=
urnFormat: "json"]) }', { patientId: patientId },
        function(data) {
            publish(divId + "_table.show-data", data);
        });
    }

    function refreshPatientIdentifiers${ id }(message, data) {
        if (data && data.activeIdentifiers)
            publish("${ id }_table.show-data", data.activeIdentif=
iers);
        else
            refreshPatientIdentifierTable('${ id }', ${ patient.patientId }=
);
    }

    subscribe('${ id }.refresh', refreshPatientIdentifiers${ id });
    subscribe('patient/${ patient.patientId }.changed', refreshPatientIdent=
ifiers${ id });
    subscribe('patient/${ patient.patientId }/identifiers.changed', refresh=
PatientIdentifiers${ id });

    subscribe('${ id }.delete-button-clicked', function(message, data) {
        if (openmrsConfirm('${ ui.message("general.confirm") }'))=
 {
            jq.post('${ ui.actionLink("deleteIdentifier") }', { r=
eturnFormat: 'json', patientIdentifierId: data },
            function(data) {
                notifySuccess('${ ui.escapeJs(ui.message("PatientIdent=
ifier.deleted")) }');
                publish('patient/${ patient.patientId }/identifiers.changed=
', data);
            }, 'json')
            .error(function() {
                notifyError("Programmer error: delete identifier faile=
d");
            })
        }
    });
</script>

<div id=3D"${ id }">
    ${ ui.includeFragment("widgets/table", [
            id: id + "_table",
            columns: [
                [ property: "identifierType", heading: ui.message=
("PatientIdentifier.identifierType") ],
                [ property: "identifier", userEntered: true, head=
ing: ui.message("PatientIdentifier.identifier") ],
                [ property: "location", heading: ui.message("=
;PatientIdentifier.location") ],
                [ actions: [
                    [ action: "event",
                        icon: "delete24.png",
                        tooltip: ui.message("PatientIdentifier.delete&=
quot;),
                        event: id + ".delete-button-clicked",
                        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.pa=
tientId ],
                [ label: ui.message("PatientIdentifier.identifierType&=
quot;), formFieldName: "identifierType", class: org.openmrs.Patie=
ntIdentifierType ],
                [ 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>
=20
=20

Handling the Add Identifier action is easy: we just need to add a "= successEvent" configuration attribute to the popupForm widget. This au= tomatically 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.

=20

Handling the Remove Identifier action is easy as well. We had already de= fined a function callback for the delete-button-clicked event that did an A= JAX post via jQuery. Now we just modify its success callback function sligh= tly (to show a success message, and publish the patient changed message) an= d specify the 'json' return type (otherwise jQuery would pass a String to o= ur success function instead of a javascript object).

=20

Once you've made those changes, you should be able to reload the page, a= nd add and remove multiple identifiers without having to do a full page rel= oad.

=20

Note: you'll probably notice a few framework bugs as you play around wit= h the add and remove, specifically:

=20 =20

Step 9: Additional validation

=20

We've actually missed a bit of necessary validation: we shouldn't blindl= y just delete the identifier without doing some checks first:

=20 =20

In case of any errors while executing a fragment action, you just need t= o return a FailureResult, and the UI framework will take care of sending it= back in the correct way. (If the fragment is called via ajax, an errors ob= ject is returned as json or xml. If the fragment is done as a regular form = submission, it is returned in a session attribute and displayed at the top = of the page.) Typically fragment actions will declare a return type of Obje= ct, so they may return either a Success/Object result, or a FailureResult.<= /p>=20

The simplest FailureResult constructor takes a single error message--tha= t's the one we'll use here.

=20

First, we add the validation to our fragment action method:

=20
Further validation in delete action
=20
/**
=09 * Fragment Action for deleting an existing identifier
=09 */
=09public Object deleteIdentifier(UiUtils ui, @RequestParam("patientId=
entifierId") Integer id,
=09                                             @RequestParam(value=3D"=
;reason", defaultValue=3D"user interface") String reason) {
=09=09PatientService ps =3D Context.getPatientService();
=09=09PatientIdentifier pid =3D ps.getPatientIdentifier(id);
=09=09// don't touch it if it's already deleted
=09=09if (pid.isVoided())
=09=09=09return new FailureResult(ui.message("PatientIdentifier.delete=
.error.already"));
=09=09// don't delete the last active identifier
=09=09if (pid.getPatient().getActiveIdentifiers().size() =3D=3D 1) {
=09=09=09return new FailureResult(ui.message("PatientIdentifier.delete=
.error.last"));
=09=09}
=09=09// otherwise, we go ahead and delete it
=09=09ps.voidPatientIdentifier(pid, reason);
=09=09return FragmentUtil.standardPatientObject(ui, pid.getPatient());
=09}
=20
=20

We also need to make one small change to the view in order to actually d= isplay those errors. Before we were flashing a generic message, that wasn't= actually helpful to the user. Instead, we just pass the jQuery xml http re= sponse (it's the first argument that jQuery passes to the failure function = for any ajax call) to a utility javascript method that the framework provid= es:

=20
changing the view to support error messages raised by the delete action = via ajax
=20
subscribe('${ id }.delete-button-clicked', function(message, data) {
        if (openmrsConfirm('${ ui.message("general.confirm") }'))=
 {
            jq.post('${ ui.actionLink("deleteIdentifier") }', { r=
eturnFormat: 'json', patientIdentifierId: data },
            function(data) {
                notifySuccess('${ ui.escapeJs(ui.message("PatientIdent=
ifier.deleted")) }');
                publish('patient/${ patient.patientId }/identifiers.changed=
', data);
            }, 'json')
            .error(function(xhr) {
                fragmentActionError(xhr, "Programmer error: delete ide=
ntifier failed");
            })
        }
    });
=20
=20

Step 10: Additional features

=20

Up until this point, we've covered the very basic functionality that pre= tty much every patient-based list of items will have. Namely, you can displ= ay the list, and add and remove items. Now let's add another feature that's= a bit custom for this particular fragment: we should visually identify whi= ch one of the identifiers is the preferred one (it should already be at the= top of the list) and allow the user to mark any non-preferred identifier a= s the newly-preferred one.

=20

Of course, we need to write a fragment action to support setting a non-p= referred identifier as preferred:

=20
Fragment Action for changing the preferred identifier
=20
/**
=09 * Fragment Action for marking an identifier as preferred
=09 */
=09public Object setPreferredIdentifier(UiUtils ui,
=09                                     @RequestParam("patientIdentifi=
erId") PatientIdentifier pid) {
=09=09PatientService ps =3D Context.getPatientService();
=09=09if (pid.isVoided())
=09=09=09return new FailureResult(ui.message("PatientIdentifier.setPre=
ferred.error.deleted"));
=09=09// silently do nothing if it's already preferred
=09=09if (!pid.isPreferred()) {
=09=09=09// mark all others as nonpreferred
=09=09=09for (PatientIdentifier activePid : pid.getPatient().getActiveIdent=
ifiers()) {
=09=09=09=09if (!pid.equals(activePid) && activePid.isPreferred()) =
{
=09=09=09=09=09activePid.setPreferred(false);
=09=09=09=09=09ps.savePatientIdentifier(activePid);
=09=09=09=09}
=09=09=09}
=09=09=09// mark this one as preferred
=09=09=09pid.setPreferred(true);
=09=09=09ps.savePatientIdentifier(pid);
=09=09}
=09=09return FragmentUtil.standardPatientObject(ui, pid.getPatient());
=09}
=20
=20

This code is straightforward. The only difference between this example a= nd those in previous steps is that we're converting the patientIdentifierId= parameter directly into a PatientIdentifier directly in the method signatu= re, using Spring's automatic type conversion. (Doing this required adding a= converter class, which is documented =EF=BB=BFType Converters.)

=20

The next step is to add a column to the table in the gsp page, which sho= ws either a preferred, or non-preferred icon. The non-preferred icon should= be clickable.

=20
additions to the view to support changing the preferred patient identifi= er
=20
...
    subscribe('${ id }.set-preferred-identifier', function(message, data) {
        jq.post('${ ui.actionLink("setPreferredIdentifier") }', {=
 returnFormat: 'json', patientIdentifierId: data },
        function(data) {
            notifySuccess('${ ui.escapeJs(ui.message("PatientIdentifie=
r.setPreferred")) }');
            publish('patient/${ patient.patientId }/identifiers.changed', d=
ata);
        }, 'json')
        .error(function(xhr) {
            fragmentActionError(xhr, "Failed to set preferred identifi=
er");
        })
    });
...
    ${ ui.includeFragment("widgets/table", [
            id: id + "_table",
            columns: [
                [ property: "identifierType", heading: ui.message=
("PatientIdentifier.identifierType") ],
                [ property: "identifier", userEntered: true, head=
ing: ui.message("PatientIdentifier.identifier") ],
                [ property: "location", heading: ui.message("=
;PatientIdentifier.location") ],
                [ heading: ui.message("PatientIdentifier.preferred&quo=
t;),
                    actions: [
                        [ action: "none",
                            icon: "star_16.png",
                            showIfPropertyTrue: "preferred" ],
                        [ action: "event",
                            icon: "star_off16.png",
                            event: id + ".set-preferred-identifier&quo=
t;,
                            property: "patientIdentifierId",
                            showIfPropertyFalse: "preferred" ]
                    ]
                ],
                [ actions: [
                    [ action: "event",
                        icon: "delete24.png",
                        tooltip: ui.message("PatientIdentifier.delete&=
quot;),
                        event: id + ".delete-button-clicked",
                        property: "patientIdentifierId" ]
                ] ]
            ],
            rows: patient.activeIdentifiers,
            ifNoRowsMessage: ui.message("general.none")
        ]) }
...
=20
=20

We add a callback function that will set a preferred identifier by doing= a jQuery ajax post, which will be activated based on a message. Then we ad= d a new column definition to the table, that contains two actions. These ac= tions are different from what we've seen before because they are marked as = being conditional by the attributes "showIfPropertyTrue" and &quo= t;showIfPropertyFalse".

=20

(Actually I just implemented this conditional display in TRUNK-2186. Gen= erally speaking, if you're writing a fragment that needs functionality that= will likely be needed elsewhere as well, you should be thinking about how = to build this functionality in a shared way.)

=20

So if we refresh our browser at this point, we'll see a new column, with= a star that is either highlighted or greyed out, depending on whether the = identifier is preferred.

=20

When I actually clicked around on these I noticed some odd behavior (now= fixed in the head revision of the project): marking an identifier as prefe= rred doesn't immediately move it to the top of the list on the ajax refresh= , but it does move to the top of the list the next time the page i= s loaded. Basically, there's a bug in the core OpenMRS API where changing t= he preferred identifier is not reflected in the patient.getaActiveIdentifie= rs() method until the next time the patient is loaded from the database. I = reported this as TRUNK-2188, and I introduced a workaround in the SimplePat= ient object used indirectly by FragmentUtil.standardPatientObject().

=20

S= tep 11: Refactoring

=20

Early on in the tutorial, we referred to splitting up our javascript fun= ctionality into that which is specific to this instance of the fragment, an= d that which can be split out into an external resource file. (Any javascri= pt we can put in an external resource file can be cached by the browser, an= d perhaps minimized by the UI framework, thus speeding up the application o= ver slow internet connections.) Let's go ahead and split that code out.

= =20

As we've written things so far, only a single function ("refreshPat= ientIdentifierTable") can be pulled out. We could rewrite some of the = other methods as well, but we'll leave that as an exercise for later. (Key = point: javascript that's being moved into a shared resource file must not k= now about the configuration of a specific instance of the fragment, nor may= it reference the fragment's id, or use the 'ui' groovy functions.)

=20

The resource file we build will be cached, but over a satellite internet= connection, even checking whether a cached resource has changed can slow d= own a page, so we're going to combine javascript functions for all= of the patient fragments into a single file.

=20

So, let's open "webapp/src/main/webapp/scripts/coreFragments.js&quo= t;, and move our function in there:

=20
moving javascript into an external resource
=20
var patientIdentifiersFragment =3D {

    refreshPatientIdentifierTable: function(divId, patientId) {
        $('#' + divId + '_table > tbody').empty();
        $.getJSON(actionLink('patientIdentifiers', 'getActiveIdentifiers', =
{ returnFormat: "json", patientId: patientId }), function(data) {
            publish(divId + "_table.show-data", data);
        });
    }

}
=20
=20

We've changed two things while moving this function here:

=20
    =20
  1. since we're going to be gathering functions for many fragments in this = file, we create a "patientIdentifiersFragment" object that contai= ns all functions for our fragment. (Just one, for now.)
  2. =20
  3. we can't call the groovy ui.actionLink method, so we use a javascript f= unction (defined in openmrs.js) instead.
  4. =20
=20

As a result of this we need to change one line in patientIdentifiers.gsp= to call this function with the "patientIdentifiersFragment." obj= ect prefix:

=20
=20
patientIdentifiersFragment.refreshPatientIdentifierTable('${ id }', ${ pati=
ent.patientId })
=20
------=_Part_1557_547142195.1495578715618--