You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by lo...@apache.org on 2020/04/02 15:27:10 UTC

[myfaces-tobago] 01/02: TOBAGO-2021: Sheet should be able to lazy load rows by scroll events

This is an automated email from the ASF dual-hosted git repository.

lofwyr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git

commit 0848847e2931ed7c78f2622c9460275fd4299d4b
Author: Udo Schnurpfeil <ud...@irian.eu>
AuthorDate: Thu Apr 2 08:36:37 2020 +0200

    TOBAGO-2021: Sheet should be able to lazy load rows by scroll events
---
 .../myfaces/tobago/component/Attributes.java       |   1 +
 .../apache/myfaces/tobago/event/SheetAction.java   |   7 +-
 .../tobago/internal/component/AbstractUISheet.java |  22 +-
 .../internal/renderkit/renderer/SheetRenderer.java |  19 +-
 .../renderkit/renderer/TreeNodeRenderer.java       |   2 +-
 .../taglib/component/SheetTagDeclaration.java      |   6 +
 .../tobago/renderkit/html/CustomAttributes.java    |  20 ++
 .../tobago/renderkit/html/DataAttributes.java      |   5 -
 .../tobago/example/demo/SheetController.java       |   9 +-
 .../080-sheet/90-lazy/Sheet_Lazy.xhtml}            |   7 +-
 .../src/main/npm/ts/tobago-sheet.ts                | 280 +++++++++++++++++++--
 11 files changed, 342 insertions(+), 36 deletions(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
index 1317934..be4813f 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/component/Attributes.java
@@ -146,6 +146,7 @@ public enum Attributes {
   labelWidth,
   large,
   layoutOrder,
+  lazy,
   left,
   level,
   lang,
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/event/SheetAction.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/event/SheetAction.java
index 07a1236..c9d04ec 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/event/SheetAction.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/event/SheetAction.java
@@ -54,7 +54,12 @@ public enum SheetAction {
   /**
    * Sorting
    */
-  sort();
+  sort,
+
+  /**
+   * A lazy load is requested
+   */
+  lazy;
 
   private String bundleKey;
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java
index fdb0841..dc134eb 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISheet.java
@@ -93,6 +93,21 @@ public abstract class AbstractUISheet extends AbstractUIData
   @Override
   public void encodeAll(FacesContext facesContext) throws IOException {
 
+    if (isLazy()) {
+      if (getRows() == 0) {
+        LOG.warn("Sheet id={} has lazy=true set, but not set the rows attribute!", getClientId(facesContext));
+      }
+      if (getShowRowRange() != ShowPosition.none) {
+        LOG.warn("Sheet id={} has lazy=true set, but also set showRowRange!=none!", getClientId(facesContext));
+      }
+      if (getShowPageRange() != ShowPosition.none) {
+        LOG.warn("Sheet id={} has lazy=true set, but also set showPageRange!=none!", getClientId(facesContext));
+      }
+      if (getShowDirectLinks() != ShowPosition.none) {
+        LOG.warn("Sheet id={} has lazy=true set, but also set showDirectLinks!=none!", getClientId(facesContext));
+      }
+    }
+
     final AbstractUIReload reload = ComponentUtils.getReloadFacet(this);
 
     if (reload != null && AjaxUtils.isAjaxRequest(facesContext) && reload.isRendered() && !reload.isUpdate()) {
@@ -202,7 +217,7 @@ public abstract class AbstractUISheet extends AbstractUIData
       return getRowCount();
     }
     final int last = getFirst() + getRows();
-    return last < getRowCount() ? last : getRowCount();
+    return Math.min(last, getRowCount());
   }
 
   /**
@@ -510,7 +525,7 @@ public abstract class AbstractUISheet extends AbstractUIData
         break;
       case prev:
         first = getFirst() - getRows();
-        first = first < 0 ? 0 : first;
+        first = Math.max(first, 0);
         break;
       case next:
         if (hasRowCount()) {
@@ -528,6 +543,7 @@ public abstract class AbstractUISheet extends AbstractUIData
         first = getFirstRowIndexOfLastPage();
         break;
       case toRow:
+      case lazy:
         first = pageEvent.getValue() - 1;
         if (hasRowCount() && first > getFirstRowIndexOfLastPage()) {
           first = getFirstRowIndexOfLastPage();
@@ -593,4 +609,6 @@ public abstract class AbstractUISheet extends AbstractUIData
   public abstract ShowPosition getShowPageRange();
 
   public abstract ShowPosition getShowDirectLinks();
+
+  public abstract boolean isLazy();
 }
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java
index f2e6c2b..72992ab 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SheetRenderer.java
@@ -58,11 +58,13 @@ import org.apache.myfaces.tobago.renderkit.css.BootstrapClass;
 import org.apache.myfaces.tobago.renderkit.css.CssItem;
 import org.apache.myfaces.tobago.renderkit.css.Icons;
 import org.apache.myfaces.tobago.renderkit.css.TobagoClass;
+import org.apache.myfaces.tobago.renderkit.html.CustomAttributes;
 import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlButtonTypes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
 import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
+import org.apache.myfaces.tobago.util.AjaxUtils;
 import org.apache.myfaces.tobago.util.ComponentUtils;
 import org.apache.myfaces.tobago.util.ResourceUtils;
 import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
@@ -71,6 +73,7 @@ import org.slf4j.LoggerFactory;
 
 import javax.el.ValueExpression;
 import javax.faces.application.Application;
+import javax.faces.component.NamingContainer;
 import javax.faces.component.UIColumn;
 import javax.faces.component.UIComponent;
 import javax.faces.component.UIData;
@@ -96,6 +99,7 @@ public class SheetRenderer extends RendererBase {
   private static final String SUFFIX_COLUMN_RENDERED = ComponentUtils.SUB_SEPARATOR + "rendered";
   private static final String SUFFIX_SCROLL_POSITION = ComponentUtils.SUB_SEPARATOR + "scrollPosition";
   private static final String SUFFIX_SELECTED = ComponentUtils.SUB_SEPARATOR + "selected";
+  private static final String SUFFIX_LAZY = NamingContainer.SEPARATOR_CHAR + "pageActionlazy";
   private static final String SUFFIX_PAGE_ACTION = "pageAction";
 
   @Override
@@ -193,6 +197,7 @@ public class SheetRenderer extends RendererBase {
           break;
         case toPage:
         case toRow:
+        case lazy:
           event = new PageActionEvent(component, action);
           final int target;
           final String value;
@@ -273,6 +278,10 @@ public class SheetRenderer extends RendererBase {
     }
     writer.writeAttribute(DataAttributes.SELECTION_MODE, sheet.getSelectable().name(), false);
     writer.writeAttribute(DataAttributes.FIRST, Integer.toString(sheet.getFirst()), false);
+    writer.writeAttribute(CustomAttributes.ROWS, sheet.getRows());
+    writer.writeAttribute(CustomAttributes.ROW_COUNT, Integer.toString(sheet.getRowCount()), false);
+    writer.writeAttribute(CustomAttributes.LAZY, sheet.isLazy());
+    writer.writeAttribute(CustomAttributes.LAZY_UPDATE, sheet.isLazy() && AjaxUtils.isAjaxRequest(facesContext));
 
     final boolean autoLayout = sheet.isAutoLayout();
     if (!autoLayout) {
@@ -337,6 +346,10 @@ public class SheetRenderer extends RendererBase {
           sheetId + SUFFIX_SELECTED);
     }
 
+    if (sheet.isLazy()) {
+      encodeHiddenInput(writer,null, sheetId + SUFFIX_LAZY);
+    }
+
     StringBuilder expandedValue = null;
     if (sheet.isTreeModel()) {
       expandedValue = new StringBuilder(",");
@@ -630,11 +643,7 @@ public class SheetRenderer extends RendererBase {
       }
 
       writer.startElement(HtmlElements.TR);
-      if (rowRendered instanceof Boolean) {
-        // if rowRendered attribute is set we need the rowIndex on the client
-        writer.writeAttribute(DataAttributes.ROW_INDEX, rowIndex);
-      }
-
+      writer.writeAttribute(CustomAttributes.ROW_INDEX, rowIndex);
       final boolean selected = selectedRows.contains(rowIndex);
       final String[] rowMarkups = (String[]) sheet.getAttributes().get("rowMarkup");
       Markup rowMarkup = Markup.NULL;
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java
index 2569227..54ee3a6 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TreeNodeRenderer.java
@@ -150,7 +150,7 @@ public class TreeNodeRenderer extends RendererBase {
       writer.writeAttribute(HtmlAttributes.VALUE, clientId, true);
       writer.writeIdAttribute(clientId);
       writer.writeAttribute(HtmlAttributes.SELECTED, selectedState.isAncestorOfSelected(path));
-      writer.writeAttribute(DataAttributes.ROW_INDEX, data.getRowIndex());
+      writer.writeAttribute(CustomAttributes.ROW_INDEX, data.getRowIndex());
     } else {
       writer.startElement(HtmlElements.TOBAGO_TREE_NODE);
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java
index d845e5b..7918612 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SheetTagDeclaration.java
@@ -260,6 +260,12 @@ public interface SheetTagDeclaration
       methodSignature = "javax.faces.event.ActionEvent")
   void setSortActionListener(String sortActionListener);
 
+  /**
+   * Lazy loading by scroll event.
+   */
+  @TagAttribute
+  @UIComponentTagAttribute(type = "boolean", defaultValue = "false")
+  void setLazy(String lazy);
 
   /**
    * Flag indicating if paging arrows are shown near direct links
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java
index 7b8030b..1f1201c 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/CustomAttributes.java
@@ -37,6 +37,14 @@ public enum CustomAttributes implements MarkupLanguageAttributes {
    */
   INDEX("index"),
   ITEMS("items"),
+  /**
+   * Lazy loading in sheet.
+   */
+  LAZY("lazy"),
+  /**
+   * Is this request/response an update of lazy loaded data in sheet.
+   */
+  LAZY_UPDATE("lazy-update"),
   LOCALE("locale"),
   LOCAL_MENU("local-menu"),
   MAX_ITEMS("max-items"),
@@ -51,6 +59,18 @@ public enum CustomAttributes implements MarkupLanguageAttributes {
    */
   RENDER("render"),
   /**
+   * Number of rows to show/load for lazy loading in sheet.
+   */
+  ROWS("rows"),
+  /**
+   * Number of all rows in sheet.
+   */
+  ROW_COUNT("row-count"),
+  /**
+   * Index of a specific row in the sheet.
+   */
+  ROW_INDEX("row-index"),
+  /**
    * The mode of the tab switch: client, reloadTab, reloadPage.
    */
   SWITCH_TYPE("switch-type"),
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
index 385f88a..41736ab 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
@@ -121,11 +121,6 @@ public enum DataAttributes implements MarkupLanguageAttributes {
    */
   RELOAD("data-tobago-reload"),
 
-  /*
-   * Holds the index of the row in a sheet, if the sheet has a rowRendered attribute.
-   */
-  ROW_INDEX("data-tobago-row-index"),
-
   SELECTION_MODE("data-tobago-selection-mode"),
 
   /**
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SheetController.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SheetController.java
index 46b529f..53fff0f 100644
--- a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SheetController.java
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/SheetController.java
@@ -83,12 +83,17 @@ public class SheetController implements Serializable {
   private void init() {
     solarList = astroData.findAll().collect(Collectors.toList());
 
+    int j = 1;
     hugeSolarList = new ArrayList<>();
-    for (int i = 1; i <= 1000; i++) {
+    for (; ; ) {
       for (final SolarObject solarObject : solarList) {
         final SolarObject solarObjectClone = new SolarObject(solarObject);
-        solarObjectClone.setName(solarObject.getName() + " (" + i + ". entry)");
         hugeSolarList.add(solarObjectClone);
+        solarObjectClone.setName("#" + j++ + " " + solarObject.getName());
+
+        if (j > 10000) {
+          return;
+        }
       }
     }
   }
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/40-test/3000-sheet/20-1000-entries/1000_Entries.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/080-sheet/90-lazy/Sheet_Lazy.xhtml
similarity index 88%
rename from tobago-example/tobago-example-demo/src/main/webapp/content/40-test/3000-sheet/20-1000-entries/1000_Entries.xhtml
rename to tobago-example/tobago-example-demo/src/main/webapp/content/20-component/080-sheet/90-lazy/Sheet_Lazy.xhtml
index e64270b..5e46039 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/content/40-test/3000-sheet/20-1000-entries/1000_Entries.xhtml
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/20-component/080-sheet/90-lazy/Sheet_Lazy.xhtml
@@ -21,10 +21,11 @@
                 xmlns="http://www.w3.org/1999/xhtml"
                 xmlns:tc="http://myfaces.apache.org/tobago/component"
                 xmlns:ui="http://java.sun.com/jsf/facelets">
-
+  <ui:param name="title" value="Lazy Loading Large Data"/>
   <tc:sheet value="#{sheetController.hugeSolarList}" id="sheet" var="luminary"
-            rows="1000" markup="small" >
-    <tc:style maxHeight="600px"/>
+            rows="20" markup="small" lazy="true"
+            showRowRange="none" showDirectLinks="none" showPageRange="none">
+    <tc:style maxHeight="500px"/>
     <tc:column label="Name">
       <tc:out value="#{luminary.name}" labelLayout="skip"/>
     </tc:column>
diff --git a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-sheet.ts b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-sheet.ts
index 375fda5..5b6604c 100644
--- a/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-sheet.ts
+++ b/tobago-theme/tobago-theme-standard/src/main/npm/ts/tobago-sheet.ts
@@ -25,6 +25,8 @@ class Sheet extends HTMLElement {
   mousemoveData: any;
   mousedownOnRowData: any;
 
+  lastCheckMillis: number;
+
   private static getScrollBarSize(): number {
     const body = document.getElementsByTagName("body").item(0);
 
@@ -48,12 +50,23 @@ class Sheet extends HTMLElement {
     return ["INPUT", "TEXTAREA", "SELECT", "A", "BUTTON"].indexOf(element.tagName) > -1;
   }
 
+  private static getRowTemplate(columns: number, rowIndex: number) : string {
+    return `<tr row-index="${rowIndex}" class="tobago-sheet-row" dummy="dummy">
+<td class="tobago-sheet-cell" colspan="${columns}"> </td>
+</tr>`;
+  }
+
   constructor() {
     super();
   }
 
   connectedCallback(): void {
 
+    if (this.lazyUpdate) {
+      // nothing to do here, will be done in method lazyResponse()
+      return;
+    }
+
     // synchronize column widths ----------------------------------------------------------------------------------- //
 
     // basic idea: there are two possible sources for the sizes:
@@ -153,7 +166,7 @@ class Sheet extends HTMLElement {
     const selectionMode = this.dataset.tobagoSelectionMode;
     if (selectionMode === "single" || selectionMode === "singleOrNone" || selectionMode === "multi") {
 
-      for (const row of this.getRows()) {
+      for (const row of this.getRowElements()) {
         row.addEventListener("mousedown", this.mousedownOnRow.bind(this));
 
         row.addEventListener("click", this.clickOnRow.bind(this));
@@ -167,6 +180,43 @@ class Sheet extends HTMLElement {
       });
     }
 
+    // lazy load by scrolling ----------------------------------------------------------------- //
+
+      const lazy = this.lazy;
+
+      if (lazy) {
+        // prepare the sheet with some auto-created (empty) rows
+        const rowCount = this.rowCount;
+        const sheetBody = this.tableBodyDiv;
+        const tableBody = this.tableBody;
+        const columns = tableBody.rows[0].cells.length;
+        let current: HTMLTableRowElement = tableBody.rows[0]; // current row in this algorithm, begin with first
+        // the algorithm goes straight through all rows, not selectors, because of performance
+        for (let i = 0; i < rowCount; i++) {
+          if (current) {
+            const rowIndex = Number(current.getAttribute("row-index"));
+            if (i < rowIndex) {
+              const template = Sheet.getRowTemplate(columns, i);
+              current.insertAdjacentHTML("beforebegin", template);
+            } else if (i === rowIndex) {
+              current = current.nextElementSibling as HTMLTableRowElement;
+            // } else { TBD: I think this is not possible
+            //   const template = Sheet.getRowTemplate(columns, i);
+            //   current.insertAdjacentHTML("afterend", template);
+            //   current = current.nextElementSibling as HTMLTableRowElement;
+            }
+          } else {
+            const template = Sheet.getRowTemplate(columns, i);
+            tableBody.insertAdjacentHTML("beforeend", template);
+          }
+        }
+
+        sheetBody.addEventListener("scroll", this.lazyCheck.bind(this));
+
+        // initial
+        this.lazyCheck();
+      }
+
     // ---------------------------------------------------------------------------------------- //
 
     for (const checkbox of <NodeListOf<HTMLInputElement>>this.querySelectorAll(
@@ -191,7 +241,212 @@ class Sheet extends HTMLElement {
         }
       });
     }
+  }
+
+  // attribute getter + setter ---------------------------------------------------------- //
 
+  get lazyActive():boolean {
+    return this.hasAttribute("lazy-active");
+  }
+
+  set lazyActive(update:boolean) {
+    if (update) {
+      this.setAttribute("lazy-active", "");
+    } else {
+      this.removeAttribute("lazy-active");
+    }
+  }
+
+  get lazy():boolean {
+    return this.hasAttribute("lazy");
+  }
+
+  set lazy(update:boolean) {
+    if (update) {
+      this.setAttribute("lazy", "");
+    } else {
+      this.removeAttribute("lazy");
+    }
+  }
+
+  get lazyUpdate():boolean {
+    return this.hasAttribute("lazy-update");
+  }
+
+  get rows():number {
+    return parseInt(this.getAttribute("rows"));
+  }
+
+  get rowCount():number {
+    return parseInt(this.getAttribute("row-count"));
+  }
+
+  get tableBodyDiv(): HTMLDivElement {
+    return this.querySelector(".tobago-sheet-body");
+  }
+
+  get tableBody(): HTMLTableSectionElement {
+    return this.querySelector(".tobago-sheet-bodyTable>tbody");
+  }
+
+  // -------------------------------------------------------------------------------------- //
+
+  /*
+    when an event occurs (initial load OR scroll event OR AJAX response)
+
+    then -> Tobago.Sheet.lazyCheck()
+            1. check, if the lazy reload is currently active
+               a) yes -> do nothing and exit
+               b) no  -> step 2.
+            2. check, if there are data need to load (depends on scroll position and already loaded data)
+               a) yes -> set lazy reload to active and make an AJAX request with Tobago.Sheet.reloadLazy()
+               b) no  -> do nothing and exit
+
+     AJAX response -> 1. update the rows in the sheet from the response
+                      2. go to the first part of this description
+  */
+
+  /**
+   * Checks if a lazy update is required, because there are unloaded rows in the visible area.
+   */
+  lazyCheck(event?): void {
+
+    if (this.lazyActive) {
+      // nothing to do, because there is an active AJAX running
+      return;
+    }
+
+    if (this.lastCheckMillis && Date.now() - this.lastCheckMillis < 100) {
+      // do nothing, because the last call was just a moment ago
+      return;
+    }
+
+    this.lastCheckMillis = Date.now();
+    const next = this.nextLazyLoad();
+    // console.info("next %o", next); // @DEV_ONLY
+    if (next) {
+      this.lazyActive = true;
+      const rootNode = this.getRootNode() as ShadowRoot | Document;
+      const input = rootNode.getElementById(this.id + ":pageActionlazy") as HTMLInputElement;
+      input.value = String(next);
+      this.reloadWithAction(input);
+    }
+  }
+
+  nextLazyLoad(): number {
+    // find first tr in current visible area
+    const rows = this.rows;
+    const rowElements = this.tableBody.rows;
+    let min = 0;
+    let max = rowElements.length;
+    // binary search
+    let i;
+    while (min < max) {
+      i = Math.floor((max - min) / 2) + min;
+      // console.log("min i max -> %d %d %d", min, i, max); // @DEV_ONLY
+      if (this.isRowAboveVisibleArea(rowElements[i])) {
+        min = i + 1;
+      } else {
+        max = i;
+      }
+    }
+    for (i = min; i < min + rows && i < rowElements.length; i++) {
+      if (this.isRowDummy(rowElements[i])) {
+        return i + 1;
+      }
+    }
+
+    return null;
+  }
+
+  isRowAboveVisibleArea(tr: HTMLTableRowElement): boolean {
+    const sheetBody = this.tableBodyDiv;
+    const viewStart = sheetBody.scrollTop;
+    const trEnd = tr.offsetTop + tr.clientHeight;
+    return trEnd < viewStart;
+  }
+
+  isRowDummy(tr): boolean {
+    return tr.hasAttribute("dummy");
+  }
+
+  lazyResponse(event): void {
+    let updates;
+    if (event.status === "complete") {
+      updates = event.responseXML.querySelectorAll("update");
+      for (let i = 0; i < updates.length; i++) {
+        const update = updates[i];
+        const id = update.getAttribute("id");
+        if (id.indexOf(":") > -1) { // is a JSF element id, but not a technical id from the framework
+          console.debug("[tobago-sheet][complete] Update after jsf.ajax complete: #" + id); // @DEV_ONLY
+
+          const sheet = document.getElementById(id);
+          sheet.id = id + "::lazy-temporary";
+
+          const page = Page.page();
+          page.insertAdjacentHTML("beforeend", `<div id="${id}"></div>`);
+          const sheetLoader = document.getElementById(id);
+        }
+      }
+    } else if (event.status === "success") {
+      updates = event.responseXML.querySelectorAll("update");
+      for (let i = 0; i < updates.length; i++) {
+        const update = updates[i];
+        const id = update.getAttribute("id");
+        if (id.indexOf(":") > -1) { // is a JSF element id, but not a technical id from the framework
+          console.debug("[tobago-sheet][success] Update after jsf.ajax complete: #" + id); // @DEV_ONLY
+
+          // sync the new rows into the sheet
+          const sheetLoader = document.getElementById(id);
+          const sheet = document.getElementById(id + "::lazy-temporary");
+          sheet.id = id;
+          const tbody = sheet.querySelector(".tobago-sheet-bodyTable>tbody");
+
+          const newRows = sheetLoader.querySelectorAll(".tobago-sheet-bodyTable>tbody>tr");
+          for (i = 0; i < newRows.length; i++) {
+            const newRow = newRows[i];
+            const rowIndex = Number(newRow.getAttribute("row-index"));
+            const row = tbody.querySelector("tr[row-index='" + rowIndex + "']");
+            // replace the old row with the new row
+            row.insertAdjacentElement("afterend", newRow);
+            tbody.removeChild(row);
+          }
+
+          sheetLoader.parentElement.removeChild(sheetLoader);
+          this.lazyActive = false;
+        }
+      }
+    }
+  }
+
+  lazyError(data): void {
+    console.error("Sheet lazy loading error:"
+        + "\nError Description: " + data.description
+        + "\nError Name: " + data.errorName
+        + "\nError errorMessage: " + data.errorMessage
+        + "\nResponse Code: " + data.responseCode
+        + "\nResponse Text: " + data.responseText
+        + "\nStatus: " + data.status
+        + "\nType: " + data.type);
+  }
+
+  // tbd: how to do this in Tobago 5?
+  reloadWithAction(source: HTMLElement): void {
+    console.debug("reload sheet with action '" + source.id + "'"); // @DEV_ONLY
+    const executeIds = this.id;
+    const renderIds = this.id;
+    const lazy = this.lazy;
+
+    jsf.ajax.request(
+        source.id,
+        null,
+        {
+          "javax.faces.behavior.event": "reload",
+          execute: executeIds,
+          render: renderIds,
+          onevent: lazy ? this.lazyResponse.bind(this) : undefined,
+          onerror: lazy ? this.lazyError.bind(this) : undefined
+        });
   }
 
   loadColumnWidths(): number[] {
@@ -358,7 +613,7 @@ class Sheet extends HTMLElement {
         window.getSelection().removeAllRanges();
       }
 
-      const rows = this.getRows();
+      const rows = this.getRowElements();
       const selector = this.getSelectorCheckbox(row);
       const selectionMode = this.dataset.tobagoSelectionMode;
 
@@ -464,7 +719,7 @@ class Sheet extends HTMLElement {
     return row.querySelector("tr>td>input.tobago-sheet-columnSelector");
   }
 
-  getRows(): NodeListOf<HTMLTableRowElement> {
+  getRowElements(): NodeListOf<HTMLTableRowElement> {
     return this.getBodyTable().querySelectorAll("tbody>tr");
   }
 
@@ -473,11 +728,7 @@ class Sheet extends HTMLElement {
   }
 
   isRowSelected(row: HTMLTableRowElement): boolean {
-    let rowIndex = +row.dataset.tobagoRowIndex;
-    if (!rowIndex) {
-      rowIndex = row.sectionRowIndex + this.getFirst();
-    }
-    return this.isSelected(rowIndex);
+    return this.isSelected(parseInt(row.dataset.tobagoRowIndex));
   }
 
   isSelected(rowIndex: number): boolean {
@@ -503,17 +754,17 @@ class Sheet extends HTMLElement {
   }
 
   selectAll(): void {
-    const rows = this.getRows();
+    const rows = this.getRowElements();
     this.selectRange(rows, 0, rows.length - 1, true, false);
   }
 
   deselectAll(): void {
-    const rows = this.getRows();
+    const rows = this.getRowElements();
     this.selectRange(rows, 0, rows.length - 1, false, true);
   }
 
   toggleAll(): void {
-    const rows = this.getRows();
+    const rows = this.getRowElements();
     this.selectRange(rows, 0, rows.length - 1, true, true);
   }
 
@@ -538,12 +789,7 @@ class Sheet extends HTMLElement {
   }
 
   getDataIndex(row: HTMLTableRowElement): number {
-    const rowIndex = parseInt(row.dataset.tobagoRowIndex);
-    if (rowIndex) {
-      return rowIndex;
-    } else {
-      return row.sectionRowIndex + this.getFirst();
-    }
+    return parseInt(row.dataset.tobagoRowIndex);
   }
 
   /**