Using inputNumberSlider for Dates

I'm currently working on a prototype User Interface for an internal project that surfaced a requirement for allowing date selection using a slider control.  ADF Faces itself only supports two forms of slider control (<af:inputNumberSlider> and <af:inputRangeSlider>) , so what to do? Well putting aside for one moment the aesthetic and usability of using a slider for date selection ( not something I wholly buy into myself), can it be done? 

The simple answer is (of course and hence the article) yes.

Is it a Date? Is it a Number? 

Fortunately it's both. Java dates are stored internally as longs so there is no fundamental issue with using the inputNumberSlider to select one, providing that we get the precision right - milliseconds are probably not that useful as an increment. However, if we try and base a inputNumberSlider on the millisecond value of a date, the main problem is going to be the labels - in fact here's what it might look like:

Time slider with default labelling

So how do we  use this component but convert the labels to something sensible such as dates in the format "MM/dd", ending up with this:

Tome slider with corrected labelling

Well to achieve that we need a custom converter which can be assigned to the converter property of the component thus:

<af:inputNumberSlider label="Pick a day" id="ins1"
    minimum="#{uiManager.dateRangeSliderStartDate}"
    maximum="#{uiManager.dateRangeSliderEndDate}"
    majorIncrement="#{uiManager.dateRangeSlider30DayIncrement}"
    minorIncrement="#{uiManager.dateRangeSlider1DayIncrement}"
    value="#{uiManager.pickedDateAsLong}"
    converter="SliderDateConverter" 
    contentStyle="width:50em;"/> 

Defining The Converter

 Before I proceed here, credit has to go to my good buddy Matthias Wessendorf who's code from this article I have freely adapted here.

To define the converter, there are three steps:

  1. Write a server side converter in Java. 
  2. Write a client-side converter in JavaScript
  3. Register the converter with Faces 

The Server Side Converter Class

The server side converter is called by the framework as it initially renders the component on the page. It will call into this class several times to generate the major tick labels and of course the label for the slider value.  The converter class needs to implement two interfaces; org.apache.myfaces.trinidad.convert.ClientConverter and javax.faces.convert.Converter. In this case I've only had to implement four methods, two of which relate to the wiring up of the client JavaScript to the converter and the others manage the conversion itself. Let's look at those latter two first. 

Converters in JSF handle the basic problem of taking a value Object  and converting it into a String form that can be sent down to the browser in HTML and then the reverse of that cam task of taking the String value that gets sent up on the request and converting that back into the Object value form. 

So in this case we're attempting to convert a Long object (for convenience I'm actually storing the value as a long and then providing a typed getter to provide the actual date value when it's asked for). The conversion will be something like this:

1335135600000 --> "04/24"

So this paired conversion is handled by two methods called getAsString() and getAsObject() and the implementations are pretty simple - just a bit of string parsing and date arithmetic / formatting. I'm using the org.apache.commons.lang.time.DateFormatUtils helper class as well:

public String getAsString(FacesContext facesContext, UIComponent uIComponent, Object valueAsObject) {
  long selectedValue =  ((Double)valueAsObject).longValue();
  return DateFormatUtils.format(selectedValue, "MM/dd");
}
public Object getAsObject(FacesContext facesContext, UIComponent uIComponent, String valueAsString) {
  Calendar cal = new GregorianCalendar();
  int currentMonth = cal.get(Calendar.MONTH); /* Zero based */
  int currentYear = cal.get(Calendar.YEAR);
        
  //Parse the supplied String assuming the format MM/dd in this case
  String[] dateBits = valueAsString.split("[/]");
  int month = Integer.parseInt(dateBits[0]) - 1;
  int day = Integer.parseInt(dateBits[1]);
  int year = currentYear;
  //Handle the situation where the span crosses a year boundary
  //In my specific use case the dates all work backwards from today
  if (month > currentMonth){
    year--;
  }
        
  //Reconstruct the actual date
  Calendar selectedAsDateCal = new GregorianCalendar(year,month,day);
  return selectedAsDateCal.getTimeInMillis();
}

So the only complexity in this case is in the case where the selected String value is something like "11/30" which, because the range of my slider extends into the past from the current date, has to be interpreted as 30th November 2011 not 30th November 2012. Of course if you are trying to create a slider that extends across multiple years you'll have to encode the year into the string as well - month and day alone will not give you enough information.

The second two methods I need to implement wire up the JavaScript. The function getClientLibrarySource() tells the framework what .js file the client converter is in and  getClientConversion() defines the name of the converter function:

public String getClientLibrarySource(FacesContext facesContext) {
  return facesContext.getExternalContext().getRequestContextPath() + 
                          "/resources/js/sliderDateConverter.js";   
}

public String getClientConversion(FacesContext facesContext, UIComponent uIComponent) {
  return ("new SliderDateReformatter()");
} 

The Client Side Converter 

 As specified above, the client converter is defined in a file called sliderDateConverter.js in my PUBLIC_HTML/resources/js directory. This converter is called as the use moves the slider around, so unlike the server side code which is used to format the labels as well, this is really just used to format the label on the selector and it's value tooltip.  The underlying logic is essentially identical to the Java version just converted to JavaScript. Again it's just a matter of methods to convert from Object to String and back. You'll note here as well, that the prototype of the SliderDateReformatter  is set to TrConverter, this is the equivalent, to implementing the Converter interface in Java terms.

function SliderDateReformatter()
{   
}

SliderDateReformatter.prototype = new TrConverter();

SliderDateReformatter.prototype.getFormatHint = function()
{
	return null;
}

SliderDateReformatter.prototype.getAsString = function(dateMillis,label) {
	var asDate  = new Date(dateMillis);
        var month = asDate.getMonth()+1; /* Again zero based */
        var day = asDate.getDate();
	return month + "/" + day;
}

SliderDateReformatter.prototype.getAsObject = function(dateString,label){
        var dateNow = new Date();
        var currentYear = dateNow.getFullYear();
        var currentMonth = dateNow.getMonth();
        var dateBits = dateString.split("/");
        var selectedMonth = (dateBits[0]) - 1;
        var selectedDay = dateBits[1];
        var selectedYear = currentYear;
        
        if (selectedMonth > currentMonth){
            selectedYear--;
        }
        var representedDate = new Date(selectedYear,selectedMonth,selectedDay);
	return representedDate.getTime();
} 

Register the Converter

 The final step is to register the converter by name in the faces-config.xml file. This allows the framework to match the reference converter="SliderDateConverter" made by the component with the actual converter class. Just edit the faces-config and set this in the Overview editor Converters page, or add it directly to the XML, thus:

<faces-config version="2.1" xmlns="http://java.sun.com/xml/ns/javaee">
  <application>
    <default-render-kit-id>oracle.adf.rich</default-render-kit-id>
  </application>
  ...
  <converter>
    <converter-id>SliderDateConverter</converter-id>
    <converter-class>oracle.demo.view.slider.SliderDateConverter</converter-class>
  </converter>
</faces-config> 

Wrap Up

So as we've seen it's not too difficult to use the inputNumberSlider to represent data that, at first glance, is not numerical.  The same technique can be used to control the tick labelling of the component, even when you are dealing with "real" numbers, for example you might want to define a slider that allows the user to pick a percentage from the range 1%-100% and map that onto an underlying value of 0.01 to 1. You'd use exactly the same technique to do so if you were writing things from scratch, however, that one's already handled for you! Just embed a <af:convertNumber type="percent"/> as a child of the component.

We can also use the same technique for <af:inputRangeSlider> as well.

Comments:

Hey Duncan .. Long time no talk. We have a requirement on my project for a DVT (?) that has a date/time selector overlaid onto 'available times'. Similar to the outlook / lotus notes available times component. Is there a DVT that does that do you know of any samples where someone may have rolled their own ?

Posted by Howie on June 25, 2012 at 05:05 PM BST #

Howie - do you have a screen shot of what you mean - email it to me.

Posted by Duncan on June 27, 2012 at 09:30 PM BST #

Post a Comment:
Comments are closed for this entry.
About

Hawaii, Yes! Duncan has been around Oracle technology way too long but occasionally has interesting things to say. He works in the Development Tools Division at Oracle, but you guessed that right? In his spare time he contributes to the Hudson CI Server Project at Eclipse
Follow DuncanMills on Twitter

Note that comments on this blog are moderated so (1) There may be a delay before it gets published (2) I reserve the right to ignore silly questions and comment spam is not tolerated - it gets deleted so don't even bother, we all have better things to do with our lives.
However, don't be put off, I want to hear what you have to say!

Search

Archives
« April 2014
MonTueWedThuFriSatSun
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    
       
Today