Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

(Prerequisite:

...

2.x

...

UI

...

Framework

...

Step

...

By

...

Step

...

Tutorial

...

for

...

Core

...

Developers

...

)

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

A "Patient fragment" is (obviously) 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.

...

:= }
Code Block
title
controller,
first
pass
/*\*
 * 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));
	}

}
{code}

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.

...

:= }
Code Block
title
view,
first
pass
<%
    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>
{code}

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:

...

:= }
Code Block
title
Publishing
our
fragment
...
// PatientFragmentExtensions
ret.put("patientFragment.patientIdentifiers", new PatientFragmentExtension("patientIdentifiers.fragment.title",
        "patientIdentifiers.fragment.description", "patientIdentifiers", null));
...
{code}

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.

...

:= }
Code Block
title
add
a
fragment
action
for
fetching
patient's
active
identifiers
/*\*
 * 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");
	}
}
{code}

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

...

:= }
Code Block
title
refresh
function
in
the
view
<%
    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>
{code}

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:

...

:= }
Code Block
title
Refresh
on
certain
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 });
} ${ 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>
{code}

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

...

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.

...

:= }
Code Block
title
Add
Identifier
fragment
action,
first
pass
	/**
	 * 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"));
	}
{code}

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.

...

:= }
Code Block
title
fragment
view,
now
with
add
button
<%
    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 });
}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 }">
    ${ 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") ] 
            ],
            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 ]
            ]
        ]) }
</div>
{code}

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

...

.)

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 Block
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"));
	}
{code}

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:

...

:= }
Code Block
title
fragment
view,
now
with
delete
button
<%
    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);
elsedata", 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 (prettyConfirm('${ ui.message("general.confirm") }')) {
            jq.post('${ ui.actionLink("deleteIdentifier") }', { returnFormat: 'json', patientIdentifierId: data },
            function(data) {
                location.reload(true);
            })
            .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 ]
            ]
        ]) }
</div>
{code}

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

...

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 Block
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());
	}
	
}
{code}

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

...

:= }
Code Block
title
View,
final
<%
    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>
{code}

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