Thursday, October 10, 2013

Pass JavaScript function via JSON. Pitfall and solution.

JSON is a lightweight data-interchange format. It is well readable and writable for humans and it is easy for machines to parse and generate. The most of JavaScript libraries, frameworks, plugins and whatever are configurable by options in JSON format. Sometimes you have to parse a JSON string (text) to get a real JavaScript object. For instance, sometimes you have to read the whole JSON structure from a file and make it available on the client-side as an JavaScript object. The JSON text should be well-formed. Passing a malformed JSON text results in a JavaScript exception being thrown.

What does "well-formed" mean? A well-formed JSON structure consists of data types string, number, object, array, boolean or null. Other data types are not allowed. An example:
{
    "name": "Max",
    "address": {
        "street": "Big Avenue 5",
        "zipcode": 12345,
        "country": "USA"
    },
    "age": 35,
    "married": true,
    "children": ["Mike", "John"]
}
You see here string values for keys "name", "street" and "country", number values for "zipcode" and "age", boolean value for "married", object for "address" and array for "children". But what is about JavaScript functions? Some scripts allow to pass functions as values. For instance as in some chart libraries:
{
    "margin": "2px",
    "colors": ["#FFFFFF", "CCCCCC"],
    "labelFormatter": function(value, axis) {return value + ' degree';}
}
If you try to parse this JSON text, you will face an error because function(value, axis) {return value + ' degree';} is not allowed here. Try to put this structure into this online JSON viewer to see that JSON format doesn't accept functions. So, the idea is to pass the function as string:
{
    "borderWidth": "2px",
    "colors": ["#FFFFFF", "CCCCCC"],
    "labelFormatter": "function(value, axis) {return value + ' degree';}"
}
This syntax can be parsed. But we have another problem now. We have to make a real JavaScript function from its string representation. After trying a lot I found an easy solution. Normally, to parse a well-formed JSON string and return the resulting JavaScript object, you can use a native JSON.parse(...) method (implemented in JavaScript 1.7), JSON.parse(...) from json2.js written by Douglas Crockford or the jQuery's $.parseJSON(...). The jQuery's method only expects one parameter, the JSON text: $.parseJSON(jsonText). So, you can not modify any values with it. The first two mentioned methods have two parameters
 
JSON.parse(text, reviver)
 
The second parameter is optional, but exactly this parameter will help us! reviver is a function, prescribes how the value originally produced by parsing is transformed, before being returned. More precise: the reviver function can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. An example:
var transformed = JSON.parse('{"p": 5}', function(k, v) {if (k === "") return v; return v * 2;});

// The object transformed is {p: 10}
In our case, the trick is to use eval in the reviver to replace the string value by the corresponding function object:
var jsonText = ".....";  // got from any source

var jsonTransformed = JSON.parse(jsonText, function (key, value) {
    if (value && (typeof value === 'string') && value.indexOf("function") === 0) {
        // we can only pass a function as string in JSON ==> doing a real function
        eval("var jsFunc = " + value);
        return jsFunc;
    }
         
    return value;
});
I know, eval is evil, but it doesn't hurt here. Well, who doesn't like eval there is another solution without it.
var jsonTransformed = JSON.parse(jsonText, function (key, value) {
    if (value && (typeof value === 'string') && value.indexOf("function") === 0) {
        // we can only pass a function as string in JSON ==> doing a real function
        var jsFunc = new Function('return ' + value)();
        return jsFunc;
    }
         
    return value;
});

Friday, October 4, 2013

PrimeFaces Extensions: drag-and-drop feature in Timeline

Since the last release of the PrimeFaces Extensions, the Timeline component allows dragging items from the outside (an external list, etc.) and dropping them onto the Timeline. The picture shows the dragging / dropping process.


When an item is dragged and dropped, an event with an end date will be created. The end date is the start date + 10% of the timeline width. This default algorithm fits the most use cases. Tip: if events are editable, you can adjust the start / end date later by double clicking on an event.

To activate the built-in drag-and-drop feature, simple add p:ajax with event="drop" to the timeline tag.
<div style="float:left;">
    <strong>Drag and drop events</strong>
    <p/>
    <p:dataList id="eventsList" value="#{dndTimelineController.events}"
                var="event" itemType="circle">
        <h:panelGroup id="eventBox" layout="box" style="z-index:9999; cursor:move;">
            #{event.name}
        </h:panelGroup>
        
        <p:draggable for="eventBox" revert="true" helper="clone" cursor="move"/>
    </p:dataList>
</div>
    
<pe:timeline id="timeline" value="#{dndTimelineController.model}" var="event"
             editable="true" eventMargin="10" eventMarginAxis="0" minHeight="250"
             start="#{dndTimelineController.start}" end="#{dndTimelineController.end}"
             timeZone="#{dndTimelineController.localTimeZone}"
             style="margin-left:135px;" snapEvents="false" showNavigation="true"
             dropActiveStyleClass="ui-state-highlight" dropHoverStyleClass="ui-state-hover">
    <p:ajax event="drop" listener="#{dndTimelineController.onDrop}" global="false" update="eventsList"/>
    
    <h:outputText value="#{event.name}"/>
    ...  other properties can be displayed too
</pe:timeline>
In this example, the objects in the list (on the left side) have properties name, start date, end date.
public class Event implements Serializable {

    private String name;
    private Date start;
    private Date end;

    // constructors, getter / setter
    ...

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        Event event = (Event) o;

        if (name != null ? !name.equals(event.name) : event.name != null) {
     return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }
}
The bean class defines the AJAX listener onDrop. The listener gets an instance of the class TimelineDragDropEvent. Besides start / end date and group, this event object also contains a client ID of the dragged component and dragged model object if draggable item is within a data iteration component.
public class DndTimelineController implements Serializable {

    private TimelineModel model;
    private TimeZone localTimeZone = TimeZone.getTimeZone("Europe/Berlin");
    private List<Event> events = new ArrayList<Event>();

    @PostConstruct
    protected void initialize() {
        // create timeline model
        model = new TimelineModel();

        // create available events for drag-&-drop
        for (int i = 1; i <= 13; i++) {
            events.add(new Event("Event " + i));
        }
    }

    public void onDrop(TimelineDragDropEvent e) {
        // get dragged model object (event class) if draggable item is within a data iteration component,
        // update event's start / end dates.
        Event dndEvent = (Event) e.getData();
        dndEvent.setStart(e.getStartDate());
        dndEvent.setEnd(e.getEndDate());

        // create a timeline event (not editable)
        TimelineEvent event = new TimelineEvent(dndEvent, e.getStartDate(), e.getEndDate(), false, e.getGroup());

        // add a new event
        TimelineUpdater timelineUpdater = TimelineUpdater.getCurrentInstance(":mainForm:timeline");
        model.add(event, timelineUpdater);

        // remove from the list of all events
        events.remove(dndEvent);

        FacesMessage msg = new FacesMessage(FacesMessage.SEVERITY_INFO,
                                            "The " + dndEvent.getName() + " was added", null);
        FacesContext.getCurrentInstance().addMessage(null, msg);
    }

    public TimelineModel getModel() {
        return model;
    }

    public TimeZone getLocalTimeZone() {
        return localTimeZone;
    }

    public List<Event> getEvents() {
        return events;
    }

    public Date getStart() {
        return start;
    }

    public Date getEnd() {
        return end;
    }
}
Please also consider the drag-and-drop related attributes dropHoverStyleClass, dropActiveStyleClass, dropAccept, dropScope. They are similar to corresponding attributes in p:droppable. There is only one difference - the datasource attribute is not needed. p:droppable in PrimeFaces has this attribute which represents an ID of an UIData component to connect with. I have never understood why we need this. It is possible to get the UIData component with a simple trick. The reading below is for advanced developers.

The drop callback from the jQuery Droppable gets an ui object with a reference to the draggable element. So we can look for a closest DOM element which is a container for an UIData component.
var params = [];

// Check if draggable is within a data iteration component.
// Note for PrimeFaces team: an additional unified style class ".ui-data" for all UIData components would be welcome here!
var uiData = ui.draggable.closest(".ui-datatable, .ui-datagrid, .ui-datalist, .ui-carousel");
if (uiData.length > 0) {
    params.push({
        name: this.id + '_uiDataId',
        value: uiData.attr('id')
    });
}

params.push({
    name: this.id + '_dragId',
    value: ui.draggable.attr('id')
});

// call the drop listener
this.getBehavior("drop").call(this, evt, {params: params, ...});
In the component itself, the current dragged object is extracted by this snippet
FacesContext context = FacesContext.getCurrentInstance();
Map<String, String> params = context.getExternalContext().getRequestParameterMap();
String clientId = this.getClientId(context);

Object data = null;
String dragId = params.get(clientId + "_dragId");
String uiDataId = params.get(clientId + "_uiDataId");

if (dragId != null && uiDataId != null) {
    // draggable is within a data iteration component
    UIDataContextCallback contextCallback = new UIDataContextCallback(dragId);
    context.getViewRoot().invokeOnComponent(context, uiDataId, contextCallback);
    data = contextCallback.getData();
}

TimelineDragDropEvent te = new TimelineDragDropEvent(this, behaviorEvent.getBehavior(), ... dragId, data);
The JSF standard method invokeOnComponent does the job. Now the data object is available in TimelineDragDropEvent. The class UIDataContextCallback is a simple implementation of the JSF ContextCallback interface.
public class UIDataContextCallback implements ContextCallback {

    private String dragId;
    private Object data;

    public UIDataContextCallback(String dragId) {
        this.dragId = dragId;
    }

    public void invokeContextCallback(FacesContext fc, UIComponent component) {
        UIData uiData = (UIData) component;
        String[] idTokens = dragId.split(String.valueOf(UINamingContainer.getSeparatorChar(fc)));
        int rowIndex = Integer.parseInt(idTokens[idTokens.length - 2]);
        uiData.setRowIndex(rowIndex);
        data = uiData.getRowData();
        uiData.setRowIndex(-1);
    }

    public Object getData() {
        return data;
    }
}

Thursday, October 3, 2013

PrimeFaces Extensions 1.0.0 released

We are glad to announce the final release 1.0.0 of PrimeFaces Extensions. The full list of closed issues is on the GitHub. This release is fully compatible with the PrimeFaces 4.0 which was also released today.

Now both, PrimeFaces and PrimeFaces Extensions are in the Maven central repo. See Getting Started how to use it.

Thanks to all for contribution.

Tuesday, September 17, 2013

Timeline component features lazy loading

The first release candidate 1.0.0.RC1 of the PrimeFaces Extensions brought one new of two planned features for the Timeline component. This component supports lazy loading of events during moving / zooming in the timeline. This makes sense when event's loading is time-consuming. Unlike Schedule component in the PrimeFaces it didn't introduce a new LazyTimelineModel. The model class stays the same - TimelineModel. Events are loaded lazy when p:ajax with event="lazyload" is attached to the pe:timeline tag. This new AJAX behaviour has several advantages in comparison to the approach with the "lazy model". It is consistent with other AJAX events such as add, delete, rangechanged, etc. You can disable / enable the "lazyload" AJAX behavior at runtime and execute a custom JS script on start / complete. Example:
<p:ajax event="lazyload" disabled="#{lazyTimelineController.disabled}"
            listener="#{lazyTimelineController.onLazyLoad}"  
            onstart="PF('dialogWidget').show()"   
            oncomplete="PF('dialogWidget').hide()"/>
And of course you can control exactly what should be executed and updated (process / update attributes). To understand how this new feature works (before posting a lot of code :-)) I sketched one diagram. Please read from top to down.


On intial page load, only events in the visible time range should be lazy loaded. After moving to the right or left direction, area on the right or left side is coming into the view. Only events in this new displayed time range should be loaded. Time range with already loaded events is cached internally (read line in the diagram). When you zoom out now, you will probably have two time ranges the events should be loaded for. This is demonstrated in the 4. use case. When you zoom in, no AJAX event for lazy loading is sent. It is very smart! As you can see, the "lazyload" listener is not invoked again when the visible time range (incl. some hidden ranges defined by preloadFactor) already has lazy loaded events.

What is preloadFactor? The preloadFactor attribute of the pe:timeline is a positive float value or 0 (default). When the lazy loading feature is active, the calculated time range for preloading will be multiplicated by the preload factor. The result of this multiplication specifies the additional time range which will be considered for the preloading during moving / zooming too. For example, if the calculated time range for preloading is 5 days and the preload factor is 0.2, the result is 5 * 0.2 = 1 day. That means, 1 day backwards and / or 1 day onwards will be added to the original calculated time range. The event's area to be preloaded is wider then. This helps to avoid frequently, time-consuming fetching of events. Note: the preload factor in the diagram above was 0.

Let me show the code now. The code is taken from this live example of the deployed showcase.
<div id="loadingText" style="font-weight:bold; margin:-5px 0 5px 0; visibility:hidden;">Loading ...</div>  
              
<pe:timeline id="timeline" value="#{lazyTimelineController.model}"  
             preloadFactor="#{lazyTimelineController.preloadFactor}"  
             zoomMax="#{lazyTimelineController.zoomMax}"  
             minHeight="170" showNavigation="true">  
    <p:ajax event="lazyload" update="@none" listener="#{lazyTimelineController.onLazyLoad}"  
            onstart="$('#loadingText').css('visibility', 'visible')"   
            oncomplete="$('#loadingText').css('visibility', 'hidden')"/>  
</pe:timeline>
You see a hidden "Loading ..." text and the timeline tag. The text is shown when a "lazyload" AJAX request is sent and gets hidden when the response is back. The bean class LazyTimelineController looks as follows
@ManagedBean
@ViewScoped
public class LazyTimelineController implements Serializable {

   private TimelineModel model;

   private float preloadFactor = 0;
   private long zoomMax;

   @PostConstruct
   protected void initialize() {
      // create empty model
      model = new TimelineModel();

      // about five months in milliseconds for zoomMax
      // this can help to avoid a long loading of events when zooming out to wide time ranges
      zoomMax = 1000L * 60 * 60 * 24 * 31 * 5;
   }

   public TimelineModel getModel() {
      return model;
   }

   public void onLazyLoad(TimelineLazyLoadEvent e) {
      try {
          // simulate time-consuming loading before adding new events
          Thread.sleep((long) (1000 * Math.random() + 100));
      } catch (Exception ex) {
          // ignore
      }

      TimelineUpdater timelineUpdater = TimelineUpdater.getCurrentInstance(":mainForm:timeline");

      Date startDate = e.getStartDateFirst(); // alias getStartDate() can be used too
      Date endDate = e.getEndDateFirst(); // alias getEndDate() can be used too

      // fetch events for the first time range
      generateRandomEvents(startDate, endDate, timelineUpdater);

      if (e.hasTwoRanges()) {
          // zooming out ==> fetch events for the second time range
          generateRandomEvents(e.getStartDateSecond(), e.getEndDateSecond(), timelineUpdater);
      }
   }

   private void generateRandomEvents(Date startDate, Date endDate, TimelineUpdater timelineUpdater) {
      Calendar cal = Calendar.getInstance();
      Date curDate = startDate;
      Random rnd = new Random();

      while (curDate.before(endDate)) {
          // create events in the given time range
          if (rnd.nextBoolean()) {
              // event with only one date
              model.add(new TimelineEvent("Event " + RandomStringUtils.randomNumeric(5), curDate), timelineUpdater);
          } else {
              // event with start and end dates
              cal.setTimeInMillis(curDate.getTime());
              cal.add(Calendar.HOUR, 18);
              model.add(new TimelineEvent("Event " + RandomStringUtils.randomNumeric(5), curDate, cal.getTime()),
                        timelineUpdater);
          }

          cal.setTimeInMillis(curDate.getTime());
          cal.add(Calendar.HOUR, 24);

          curDate = cal.getTime();
      }
   }

   public void clearTimeline() {
      // clear Timeline, so that it can be loaded again with a new preload factor
      model.clear();
   }

   public void setPreloadFactor(float preloadFactor) {
      this.preloadFactor = preloadFactor;
   }

   public float getPreloadFactor() {
      return preloadFactor;
   }

   public long getZoomMax() {
      return zoomMax;
   }
}
The listener onLazyLoad gets an event object TimelineLazyLoadEvent. The TimelineLazyLoadEvent contains one or two time ranges the events should be loaded for. Two times ranges occur when you zoom out the timeline (as in the screenshot at the end of this post). If you know these time ranges (start / end time), you can fetch events from the database or whatever. Server-side added events can be automatically updated in the UI. This happens as usally by TimelineUpdater: model.add(new TimelineEvent(...), timelineUpdater).

I hope the code is self-explained :-).


Monday, September 16, 2013

PrimeFaces Extensions 1.0.0.RC1 released

We are glad to announce the first release candidate 1.0.0.RC1 of PrimeFaces Extensions. This release is fully compatible with the PrimeFaces 4.0.RC1 (codename SENTINEL). Once the PrimeFaces 4.0 final is released, we will also release the 1.0.0 final version.

This is a big step forwards. The community can use this release with JSF 2.0 and JSF 2.2. The full list of closed issues is available on the GitHub.

Among other changes please consider the removing of ResetInput component since this functionality is now available in the PrimeFaces 4.0 and JSF 2.2. CKEditor was updated to the version 4, the Exporter and BlockUI components got many improvements. I have already blogged about some significant changes in BlockUI. The next post will be about new features in the Timeline component. It is e.g. capable to load events lazy.

Stay tuned and have fun!