Primefaces Datatables are very versatile, but when it comes to certain features, the PF Team is very stubborn as not to release a feature unless it is needed by many. One such problem that I had was when I worked on the bidirectional scroll feature for LiveScroll or the On-Demand Data. They already had the forward scroll implemented, but did not release a version for the backward scroll. So, I ended up making my own implementation. Here goes.
For simplicity, I built my table with only two columns. My model is as follows -
Car.java
It contains only two fields, name and other, which are also the columns of my table. My lazy datamodel is as follows.
DataModel.java
To perform lazy loading, it is important that the model class must extend the LazyDataModel<T> class. The load() method of the class is overridden so that we can compose the list on every callback and return it to the view. Here, it returns the next list when the bottom of the viewport/frame is reached. The loadPreCar() method is where the customization is occurring. In this method, we generate the previous set of list occurring before the first record. This list is to be displayed when the backward live scrolling is performed.
The following is my bean.
DataBean.java
This class merely holds the datamodel and provides the getter and the setter for it.
Next comes my form.
testDatatable.xhtml
If you notice, it contains three components - a datatable, a hidden variable and a remote command. The Javascript portion of the code is as follows -
It isn't as complex as it seems. Here, the scroll event simply fires the remote command method when the scroll bar is towards the top (for scroll position less than 10). The delay prevents the event from firing for every point in the scrollbar. The lastScrollTop variable keeps track of the direction of the scroll. The event fires only when the scrolling is happening in the upward direction.
The addRows() method adds the returned string from the backend to the beginning of the table. It also increments the data-ri, which is the row index attribute each row, by the number of newly added rows.
The flow -
When the page loads for the first time, the PostConstruct in the DataBean.java creates an instance of the DataModel.java. As the table is loaded, it fires the load() method of the DataModel and creates the table with the first set of rows. Scrolling down functionality will behave in the expected way as for Primefaces. The Scrolling up functionality is where the magic occurs.
When the upper region of the datatable viewport/frame is reached, the js scroll event is fired which in turn triggers the remoteCommand to fire the method loadPreCar() in the class DataModel.java. This fetches the records and formats them into <tr> tags, thus forming a big string of records. This string is stored in the hidden variable rowString in the dataModel. The onComplete attribute in the remoteCommand tag fires the JS method addRows() after the execution completion of the backing bean. In this method, the generated string is retrieved from the hidden variable and is simply appended to the beginning of the table. A small adjustment to the row indices is done to all the existing rows.
This functionality works very similarly to the live downward scroll feature of primefaces. The add row method is of linear complexity to the size of the table. So, with a very large tables, performance may be affected due to the adjustment in the attributes of the existing rows. Anyway, Hope this helps!
For simplicity, I built my table with only two columns. My model is as follows -
Car.java
public class Car { private String name; private String other; public Car(String name, String other) { this.name = name; this.other = other; } //Getters and Setters }
It contains only two fields, name and other, which are also the columns of my table. My lazy datamodel is as follows.
DataModel.java
public class DataModel extends LazyDataModel<Car> { List<Car> listVals; private List<Car> datasource; private int count = 200; private int backCount = 200; private String rowString = ""; private static final long serialVersionUID = 1L; public DataModel() { datasource = new ArrayList<>(); this.setRowCount(1000); } //Getter and Setter for rowString and listVals @Override public Car getRowData(String rowKey) { try { for (Car listVal : listVals) { if (listVal.getName().equals(rowKey)) { return listVal; } } } catch (ArrayIndexOutOfBoundsException ex) { ex.printStackTrace(); } return null; } @Override public Object getRowKey(Car car) { return car.getName(); } @Override public List<Car> load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters) { listVals = new ArrayList<>(); System.out.println("loading"); int end = (count + 50); for (int i = count; i < end && count <= 5000; i++, count++) { listVals.add(new Car("lamborghini" + count, "other" + count)); } datasource.addAll(listVals); return listVals; } public void loadPreCar() { listVals = new ArrayList<>(); System.out.println("loading pre"); int end = backCount; rowString=""; for (int i = backCount - 50; i < end && backCount >= 0; i++, backCount--) { listVals.add(new Car("lamborghini" + i, "other" + i)); } datasource.addAll(0, listVals); for (int i = 0; i < listVals.size(); i++) { Car car = listVals.get(i); rowString = rowString + "<tr class=\"ui-widget-content ui-datatable-even\" role=\"row\" data-ri=\"" + i + "\">" + "<td role=\"gridcell\">" + car.getName() + "</td>" + "<td role=\"gridcell\">" + car.getOther() + "</td>" + "</tr>"; i++; car = listVals.get(i); rowString = rowString + "<tr class=\"ui-widget-content ui-datatable-odd\" role=\"row\" data-ri=\"" + i + "\">" + "<td role=\"gridcell\">" + car.getName() + "</td>" + "<td role=\"gridcell\">" + car.getOther() + "</td>" + "</tr>"; } } }
To perform lazy loading, it is important that the model class must extend the LazyDataModel<T> class. The load() method of the class is overridden so that we can compose the list on every callback and return it to the view. Here, it returns the next list when the bottom of the viewport/frame is reached. The loadPreCar() method is where the customization is occurring. In this method, we generate the previous set of list occurring before the first record. This list is to be displayed when the backward live scrolling is performed.
The following is my bean.
DataBean.java
import java.io.Serializable; import javax.annotation.PostConstruct; import javax.enterprise.context.SessionScoped; import javax.inject.Named; import org.primefaces.model.LazyDataModel; @Named @SessionScoped public class DataBean implements Serializable { private static final long serialVersionUID = 1L; private String value; LazyDataModel<Car> data = null; public DataBean() { } public LazyDataModel<Car> getData() { return data; } public void setData(LazyDataModel<Car> data) { this.data = data; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } @PostConstruct public void init() { data = new DataModel(); } }
Next comes my form.
testDatatable.xhtml
<h:form id="mainForm"> <pf:growl id="growl"/> <pf:panel> <pf:dataTable var="car" scrollable="true" liveScroll="true" scrollHeight="300" scrollRows="50" value="#{dataBean.data}" id="carTable" lazy="true" > <pf:column headerText="Name"> <h:outputText value="#{car.name}" /> </pf:column> <pf:column headerText="Other"> <h:outputText value="#{car.other}" /> </pf:column> </pf:dataTable> </pf:panel> <h:inputHidden value="#{dataBean.data.rowString}" id="rowString"/> <pf:remoteCommand name="myRemote" actionListener="#{dataBean.data.loadPreCar()}" oncomplete="addRows()" update="rowString" /> </h:form>
<script type="text/javascript"> /* <![CDATA[ */ var lastScrollTop = 0; var delay = (function() { //Adding delay var timer = 0; return function(callback, ms) { clearTimeout(timer); timer = setTimeout(callback, ms); }; })(); $(document).ready(function() { $('#mainForm\\:carTable .ui-datatable-scrollable-body').on('scroll', null, function() { var scrollLocation = $('#mainForm\\:carTable .ui-datatable-scrollable-body').prop('scrollTop'); if (scrollLocation < 10) { var scrollB = $('#mainForm\\:carTable .ui-datatable-scrollable-body') if (scrollB.scrollTop() < lastScrollTop) { delay(function() { myRemote(); }, 300); } lastScrollTop = scrollB.scrollTop(); } }); }); function addRows() { var rows = $('#mainForm\\:carTable .ui-datatable-scrollable-body table tr'); for (i = 0; i < rows.length; i++) { var attrVal = parseInt(rows[i].getAttribute('data-ri')) + 50; rows[i].setAttribute('data-ri', attrVal); } var firstRow = $('#mainForm\\:carTable .ui-datatable-scrollable-body table tr:first'); var rowHeight = firstRow.height(); firstRow.before(document.getElementById('mainForm:rowString').value); var scrollB = $('#mainForm\\:carTable .ui-datatable-scrollable-body') scrollB.scrollTop('' + rowHeight * 50); lastScrollTop = rowHeight * 50; } /* ]]> */ </script>
It isn't as complex as it seems. Here, the scroll event simply fires the remote command method when the scroll bar is towards the top (for scroll position less than 10). The delay prevents the event from firing for every point in the scrollbar. The lastScrollTop variable keeps track of the direction of the scroll. The event fires only when the scrolling is happening in the upward direction.
The addRows() method adds the returned string from the backend to the beginning of the table. It also increments the data-ri, which is the row index attribute each row, by the number of newly added rows.
The flow -
When the page loads for the first time, the PostConstruct in the DataBean.java creates an instance of the DataModel.java. As the table is loaded, it fires the load() method of the DataModel and creates the table with the first set of rows. Scrolling down functionality will behave in the expected way as for Primefaces. The Scrolling up functionality is where the magic occurs.
When the upper region of the datatable viewport/frame is reached, the js scroll event is fired which in turn triggers the remoteCommand to fire the method loadPreCar() in the class DataModel.java. This fetches the records and formats them into <tr> tags, thus forming a big string of records. This string is stored in the hidden variable rowString in the dataModel. The onComplete attribute in the remoteCommand tag fires the JS method addRows() after the execution completion of the backing bean. In this method, the generated string is retrieved from the hidden variable and is simply appended to the beginning of the table. A small adjustment to the row indices is done to all the existing rows.
This functionality works very similarly to the live downward scroll feature of primefaces. The add row method is of linear complexity to the size of the table. So, with a very large tables, performance may be affected due to the adjustment in the attributes of the existing rows. Anyway, Hope this helps!
No comments:
Post a Comment