...
(Prerequisite:
...
...
...
...
...
...
...
...
...
...
...
)
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 | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| =
|
|
| }|||||||
/*\* * 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 | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| =
|
|
| }|||||||
<% 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
...
...
...
...
,
...
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 | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| =
|
|
| }|||||||
... // 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 | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| =
|
|
|
|
|
|
|
|
| }|||||||||||||
/*\* * 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 | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| =
|
|
|
|
| }|||||||||
<% 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 | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| =
|
|
|
| }||||||||
<% 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 | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| =
|
|
|
|
|
| }||||||||||
/**
* 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 | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| =
|
|
|
|
|
| }||||||||||
<% 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 | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| =
|
|
|
|
|
| }||||||||||
/**
* 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 | ||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| =
|
|
|
|
|
| }||||||||||
<% 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 | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| =
|
|
| }|||||||
/** * 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 | ||||||||
---|---|---|---|---|---|---|---|---|
| =
|
| }||||||
<% 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