Sunday, June 9, 2013

Sweat, blood and tears with JSF

I'll start with the conclusion: if you're building standard, textbook web applications with Java Server Faces, you're going to love the technology. But the moment you start developing things that are a bit off the beaten track, the fun begins .... Let's take for example the calendar I presented recently. I want to add it some more functionality: when clicking on a certain day, you're redirected to a new page where you can create a new appointment. Once you confirm it, you'll be returned to the calendar page. So, this is how I extended the jQuery calendar:


dayClick: function(date, allDay, jsEvent, view) {
     if (allDay) {
        alert('Clicked on the entire day: ' + date);
        var url = "http://localhost:8080/Patients/appointments.jsf?day=" + date.getDate() + "&month=" + (date.getMonth() + 1) + "&year=" + date.getFullYear();    
        $(location).attr('href',url);  
     } else {
        //alert('Clicked on the slot: ' + date);
     }

}
Note that I've hardcoded the path to the appointment page ("appointments.jsp"). More importantly, I'm passing the date (day, month, year) as URL request parameters, e.g.: http://localhost:8080/Patients/appointments.jsf?day=5&month=7&year=2013

On the appointments.jsp page, one of the things I do is to print the chosen date:
<%
out.println("Data: " + request.getParameter("day") + "-" + request.getParameter("month") + "-" + request.getParameter("year"));
%>
Then, I've added a text input and a button, so that the user can fill in a patient name (or part of it) and click the button in order to search (in a DB) for all patients with the name containing the given text. The patients are supposed to be displayed in a selectOneListBox, as shown below:
<h:inputText binding="#{PatientsSearch.inputText1}" valueChangeListener="#{PatientsSearch.lookForName}" immediate="true" id="inputText1"/>
<h:commandButton value="Patient search:" action="#{PatientsSearch.getPatientsByName}">
</h:commandButton>

<h:selectOneListbox id="patients" value="#{PatientsSearch.selectedPatient}">
<f:selectItems value="#{PatientsSearch.patients}" var="p"
       itemLabel="#{p.name}:#{p.phoneMobile}" itemValue="#{p.name}" />
</h:selectOneListbox>

Almost everything works fine, i.e. if you click the "Patient search" button, the listbox will indeed be filled in with the corresponding patients' names and thus you can select on of the names from the list. I'm saying "almost", because if you change your mind and look up another patient name, clicking the "Patient search" button doesn't produce any result (actually I debugged it and noticed that the handler PatientsSearch.getPatientsByName is indeed called, but somehow the listbox is not refreshed with the new names). If you don't select any name in the list and perform a new patient search, this will work (so clicking the button produces the desired result). It looks as if the listbox is not just a "select one", but "use only once": the moment you select an item from the list, you can't refresh it anymore ....
Anyway, I actually didn't like the list, so I've replaced it with a table:
<h:dataTable var="patient" value="#{PatientsSearch.patients}" style="width:300px;height:150px" rowClasses="oddRow, evenRow">
  <h:column id="name">
    <f:facet name="header">
    <h:outputText value="Name" />
    </f:facet>
    <h:outputText value="#{patient.name}" />
  </h:column>
  <h:column id="phone">
    <f:facet name="header">
    <h:outputText value="Phone" />
    </f:facet>
    <h:outputText value="#{patient.phoneMobile}" />
  </h:column>
  <h:column>
    <h:inputHidden id="patientId" value="#{patient.id}"/>
  </h:column>
</h:dataTable>
I want to show the patient's name in the first column and the patient's phone number in the second. Wait a minute, there are three columns .... The third column contains an inputHidden field, whose value is the patient's ID - patient.id. So, what's the deal with this one ? Well, when I click a row in this table, I want that patient to be selected. Since the patient's ID uniquely identifies each patient, I can't simply use the name (it might not be unique). So I'm going to keep the patient's ID in this hidden field and when the user selects a patient, this ID will be sent to the managed bean that takes care of creating the new appointment using the patient's ID. Compare this with the selectOneListbox above: when I was selecting a patient in the listbox, the selected Patient instance (as a Java type) was passed to the managed bean, as the field PatientsSearch.selectedPatient. So I'm losing this elegance when working with the table, but ok ....

Next, let's do what I've just said: when the user selects a patient from the list, the patient's ID needs to be sent to a backing bean (i.e. we need to preserve it in order to create the appointment later). Again, look how simple was to do that with the selectOneListbox: just add value="#{PatientsSearch.selectedPatient}" and you're done. This is the intrinsic functionality of the listbox. The table however is meant rather for listing data, such as the patients in our case, not necessarily for selecting it. After googling around for a while, I've arrived to the conclusion that all I need to do is:
1. use some jQuery/JavaScript functionality to select a row in the table (and highlight the selection)
2. use a JSF inputHidden field to keep the patient's ID of the selected row

These two steps are shown below. Note that I first reset the background of all "tr" (except for the first, which is the header of the table and thus excluded from any selection) and then I set the background of the selected row to orange.

After that I do step 2 - this single line of code cost me many hours of searches online: $("#j_id_id13\\:testId").val($(this).find('td:last').find('input').attr("value"));
Note that the id of the inputHidden is "testId", but JSF prefixes it with "j_id_id13" when generating the corresponding HTML input field - you can see that by showing the page source in the browser. "\\" are needed to escape ":". Then, $(this).find('td:last') means that I want to go to the last column (td) in the current row (this). Finally, I look for "input" (the HTML field generated) and read its "value" attribute - this is the selected patient's ID.
Another important note: I've displayed the value of the just set hidden field in an "alert", since viewing the browser source doesn't show the hidden field being updated.

$("tr").not(':first').click(
    function () {
      $(this).parent().find("tr").css("background", "");
      $(this).css("background","orange");
      $("#j_id_id13\\:testId").val($(this).find('td:last').find('input').attr("value"));
      alert($("#j_id_id13\\:testId").val());
    }
);

<h:inputHidden id="testId"/>

3. once I have selected the patient (a row in the table) and I click the button "Create appointment", I can retrieve the patient's ID from the hidden field with the following handler in the backing bean:
FacesContext context = FacesContext.getCurrentInstance();  
Map requestMap = context.getExternalContext().getRequestParameterMap();  
String value = (String)requestMap.get("j_id_id13:testId");
        
Patient patient;
if (value != null) {
    patient = getPatientById(value);
}
return "toCalendar";

Now I'm almost done. "Almost" again. Remember I'm on the page "appointments.jsp" and here I click "Patient search". What happens is that I'm basically sent back to the same page in the browser, but then the date line I've shown at the beginning will only display nulls:
<%
out.println("Data: " + request.getParameter("day") + "-" + request.getParameter("month") + "-" + request.getParameter("year"));
%>
That happens because when clicking the button, I'm actually creating a new request for this page, losing thus the URL parameters encoded from the calendar page. So, here's what I can do to preserve the date:
1. write the date into a cookie
2. use again hidden fields to pass the date around
3. do URL rewriting with an onclick JavaScript handler when clicking the "Patient search" button
4. use some form of browser storage (see HTML5, etc.)

And the ideas could just flow like crazy hadn't I found the simplest method:
<h:commandButton value ="Patient search:" action="#{PatientsSearch.getPatientsByName}">
    <f:param name="day" value="#{request.getParameter('day')}" />
    <f:param name="month" value="#{request.getParameter('month')}" />
    <f:param name="year" value="#{request.getParameter('year')}" />  
</h:commandButton>
Note here the EL expressions used to transmit the request parameters to the f:param and thus keep them in the request scope.
That's all folks ! Pffffffffff ....

P.S. I know you'll say there are many JSF plugins which can implement this easier, but sometimes I'm just too conservative ....

No comments:

Post a Comment