Lazy Loading Columns with Intersection Observer (SAPUI5)

Hello World!
This is my first blog! I recently had a requirement where the client wanted 365 columns in a table. The goal was to edit the schedule of multiple employees in one table over the period of a year. Another requirement was to scroll horizontally (just like Excel). Although the Fiori guidelines clearly states (source):

Try to avoid horizontal scrolling in the default delivery

Try to minimize the number of columns

To comply with these requirement I wanted to find a way to fix the performance. The issue lies in loading the app. I needed to find a way to lazy load the columns, so the app doesn’t open with 365 columns all at once. I know there is lazy loading for records, but there doesn’t seem to be lazy loading for columns. So this is my workaround. Let’s start this up!

Part 1: Create Table

(scroll down to part 2 if you already have a table)

Create a new project and download this json file (for mock data). You can put it in a folder if you like, I put it a new folder named ‘json’.


Add the json to the manifest as a datasource and as a model:

"": { "dataSources": { "yearJson": { "uri": "json/data.json", "type": "JSON" } },
... "models": { "i18n": { "type": "sap.ui.model.resource.ResourceModel", "settings": { "bundleName": "lazyload.zLazyLoad.i18n.i18n" } }, "YearModel": { "type": "sap.ui.model.json.JSONModel", "dataSource": "yearJson" } }, 

First create a table in the view. For padding purposes I also added dynamic page:

<mvc:View controllerName="lazyload.zLazyLoad.controller.start" xmlns:mvc="sap.ui.core.mvc" displayBlock="true" xmlns="sap.m" xmlns:f="sap.f" xmlns:ui="sap.ui.table"> <App id="app"> <f:DynamicPage id="mainPage"> <f:content> <ui:Table id="scheduleTableYear" fixedColumnCount="2" selectionMode="None" rows="{YearModel>/employees}"></ui:Table> </f:content> </f:DynamicPage> </App>

For the purpose of this table it’s practical to add Momentjs (there are multiple ways to add Momentjs, but i’ve added it directly as a file). You can download the file here and place it in a new folder ‘libs’. Add momentjs and JSONModel at the top and make the controller globally accessible. Also add two functions setScreenModel and createYearTable which we are going to write next:

var mainController; sap.ui.define([ "sap/ui/core/mvc/Controller", "./../libs/moment", "sap/ui/model/json/JSONModel",
], function (Controller, momentjs, JSONModel) { "use strict"; return Controller.extend("lazyload.zLazyLoad.controller.start", { onInit: function () { mainController = this; this.setScreenModel(); this.createYearTable(); }

Next, create the columns dynamically, because writing 365 columns in the xml view seems like a lot of work 😉

createYearTable: function () { this.oTableYear = this.getView().byId("scheduleTableYear"); /********************** * 1. add team /name * **********************/ this.oTableYear.addColumn(new sap.ui.table.Column({ label: "Team", template: new sap.m.Text({ text: "{YearModel>TEAM}" }) })); this.oTableYear.addColumn(new sap.ui.table.Column({ label: "Naam", template: new sap.m.Text({ text: "{YearModel>NAME}" }) })); /***************** * 2. add days * ****************/ const dateCurrentYear = new Date(2022, 0, 1); const dateNextYear = new Date(2022 + 1, 0, 1); do { const month = moment(dateCurrentYear).locale('nl').format('MMMM').toUpperCase(); const fieldDay = `{YearModel>${month}/DAY${dateCurrentYear.getDate()}}`; const newInput = new sap.m.Input({ value: fieldDay }).addStyleClass("select--scheduletype"); let columnVisible = `month${dateCurrentYear.getMonth() + 1}`; this.oTableYear.addColumn(new sap.ui.table.Column({ visible: "{screenModel>/" + columnVisible + "}", width: "110px", headerSpan: mainController.getHeaderSpan(dateCurrentYear), multiLabels: [ new sap.m.Label({ text: moment(dateCurrentYear).locale('nl').format('MMM D') }), new sap.m.Label({ text: moment(dateCurrentYear).locale('nl').format('dd') }), new sap.m.Label({ text: `week ${moment(dateCurrentYear).locale('nl').isoWeek()}` }) ], template: newInput })); dateCurrentYear.setDate(dateCurrentYear.getDate() + 1); } while (dateCurrentYear.getFullYear() < dateNextYear.getFullYear()); },

Adding headerspan for the weeks and days:

getHeaderSpan: function (oDay) { let headerSpan = [0, 0, 7]; let count = 1; const countDate = new Date(oDay); if (oDay.getMonth() === 0 && oDay.getDate() <= 7 && moment(oDay).locale("nl").isoWeek() !== 1) { do { countDate.setDate(countDate.getDate() + 1); if (moment(oDay).locale("nl").isoWeek() === moment(countDate).locale("nl").isoWeek()) { count++; } } while (moment(oDay).locale("nl").isoWeek() === moment(countDate).locale("nl").isoWeek()); headerSpan = [0, 0, count]; } return headerSpan; },

In a new model, track which month is visible. The first month is true because that month is visible when we open the app:

setScreenModel: function () { const screenData = { "month1": true, "month2": false, "month3": false, "month4": false, "month5": false, "month6": false, "month7": false, "month8": false, "month9": false, "month10": false, "month11": false, "month12": false }; const screenModel = new JSONModel(screenData); this.getView().setModel(screenModel, "screenModel"); },

Our table should now be visible but it should only be showing one month:


Part 2: Create Intersection Observer

Now we are coming to the good part. We are creating the IntersectionObserver. This function will watch the last cell of the first row. Whenever this cell enters the viewport, the cell is therefore intersecting and we can show the next month by changing the screenModel .

observeColumns: function () { = new IntersectionObserver(entries => { entries.forEach(entry => { const intersecting = entry.isIntersecting; if (intersecting) { for (let i = 1; i < 13; i++) { let month = `month${i}`; if (mainController.getView().getModel("screenModel").getData()[month] === false) { mainController.getView().getModel("screenModel").getData()[month] = true; mainController.getView().getModel("screenModel").refresh();; break; } } } }); }); },

Call observeColumns in onInit:

onInit: function () { mainController = this; this.setScreenModel(); this.observeColumns(); this.createYearTable(); },

Add intersectionObserver to table in the onAfterRendering function:

createYearTable: function () { this.oTableYear = this.getView().byId("scheduleTableYear"); this.oTableYear.addEventDelegate({ onAfterRendering: function () { if (mainController.oTableYear.getRows().length === 0) { return; } if (mainController.oTableYear.getRows()[0].getCells().length !== 0) { const rowLength = mainController.oTableYear.getRows()[0].getCells().length - 1;[0].getCells()[rowLength].getDomRef()); } } });

The months should load whenever you horizontally scroll to the end.


Final words

To summarize in this blog post you have learned to build a grid table with a large amount columns dynamically and added an intersection observer so you are able to lazy load columns and hopefully improve performance.

You can download the project here.

Let me know your feedback and if you have any questions. I’m looking forward to hear from you.

Please continue using the community to share your learnings and best practices. You can also follow me in the community if you like my content.

Useful links:

SAPUI5 Topic Page

SAPUI5 Blogs