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!

Tuesday, September 3, 2013

New features in BlockUI of PrimeFaces Extensions

PrimeFaces users were worried if we will keep the BlockUI component in the PrimeFaces Extensions. Yes, we will keep it. It was done compatible with the upcoming PrimeFaces 4.0 and got some updates / new features.

The main features:
  • Page blocking. The entire page can be blocked if you don't define the target attribute.
  • Non-centered message. A non-centered message is defined by the css and centerX / centerY attributes. Furthermore, you can use the cssOverlay attribute to style the overlay (half-transparent mask).
  • Auto-unblocking. The timeout attribute is helpful for auto-unblocking. It defines time in millis to wait before auto-unblocking.

All featutes are demonstrated in the following code snippet and the screenshot
<p:commandButton value="Block this page!" type="button"
                 onclick="PF('blockUIWidget').block()"/>

<pe:blockUI widgetVar="blockUIWidget"
            css="{top: '10px', left: '', right: '10px', cursor: 'wait'}"
            cssOverlay="{backgroundColor: 'red'}"
            timeout="2000"
            centerY="false">
    <h:panelGrid columns="2">
        <h:graphicImage library="images" name="ajax-loader1.gif"
                        style="margin-right: 12px; vertical-align: middle;"/>
        <h:outputText value="This is a non-centered message. Please wait..." style="white-space: nowrap;"/>
    </h:panelGrid>
</pe:blockUI>


 Have fun!