Awesome! So I was able to successfully implement a virtual list with sticky headers with the help of the hint you provided me above @nolimits4web.
I was reading on other threads within this forum that the handler function for the itemsAfterInsert
event provides both the virtualList
and the fragment
parameters, but the fragment
is always an empty HTML collection by the time the itemsAfterInsert
event has been fired because all of its nodes have already been added to the virtual list. This makes since.
So I instead hooked to the itemsBeforeInsert
event, which provides me the HTML fragment
with all of the nodes about to be inserted. Now, in order to get this to work, I had to employ a number of tricks (they are fairly neat and not too hackish).
I made a working example available:
https://mbplautz.github.io/example/f7-virtuallist-stickyheader-demo.html
Tested on Chrome 69
Tricks to get a virtual list to have sticky headers
This is just FYI to anybody else reading this thread to explain my solution:
- For sticky headers to work in Framework7 in any list, you have to follow a very specific DOM structure with your list, and each section that is headered. It resembles:
<div class="list links-list">
<div class="list-group">
<ul>
<li class="list-group-title">First Group Title</li>
<li><a href="#">First Group Item 1</a></li>
<li><a href="#">First Group Item 2</a></li>
</ul>
</div>
<div class="list-group">
<ul>
<li class="list-group-title">Second Group</li>
<li><a href="#">Second Group Item 1</a></li>
<li><a href="#">Second Group Item 2</a></li>
</ul>
</div>
</div>
- This DOM structure is different from a typical list because instead of having just one single parent
ul
element, you have several ul
elements – one for each group – each of them wrapped within their own div.list-group
.
- As a result, if you want sticky headers, when the virtual list is rendered, you cannot just add
li
elements to the virtual list.
- Instead, you have to add
li
elements wrapped into a <div class="list-group"><ul>...</ul></div>
section, and you have to have one section per sticky header.
- In the
itemsBeforeInsert
event, you are given the HTML fragment
of all of the rendered items. Remove each item from the HTML fragment
, add a section wrapper to the HTML fragment
, add* the sticky header as the first list item in the section wrapper, then add all of the rendered items after the header to the section wrapper.
- In the
itemsBeforeInsert
event, when you are removing and adding the rendered items, you will have to ensure that each item gets added back to the correct section wrapper in which it belongs – that is, under the correct sticky header
- Because all we are doing is editing the HTML
fragment
, there is still the parent ul
element it is being added to. This results in a DOM structure that resembles:
<div class="list virtual-list links-list">
<ul>
<div class="list-group">
<ul>
<li class="list-group-header">{{ Header name }}</li>
{{ Added rendered items }}
</ul>
</div>
</ul>
</div>
- This results in extra left padding in the list, due to the default Framework7 CSS rules on two levels of
ul
elements within a div.list
. You need to add a CSS rule to get rid of this padding.
- If you look at the DOM as you scroll through a virtual list, within the
ul
element has been added to the parent div.virtual-list
element, and you will see that within that ul
there are a shifting window of items rendered from the defined itemTemplate
attribute (or renderItem
if that was used instead). As you scroll down, the top
CSS attribute of each of these items changes with each window of rendered items.
- Since we are removing each of the rendered items from the HTML
fragment
and adding each of them to the header section wrapper within the HTML fragment
, each rendered item does not need its top
style set individually. Instead, the section wrapper needs its top
style set (we can use the top
value from the first item, since they are all the same within each window) and the top
style attribute on each rendered item needs to be cleared.
Once you’ve done all of that, you can have a virtual list with sticky headers.
The code
Styles:
.moving-div {
position: relative;
}
.list.virtual-list ul div.list-group.moving-div ul {
padding-left: 0;
}
HTML:
<div class="list links-list virtual-list">
</div>
Javascript:
This assumes you start with some sort of hierarchical data that gets flattened out
// Build a flat list from the hierarchy data
// Assume hierarchyData is an array of objects with properties name: String and subItems: Array
// Assume each subItem in the subItems array is an object with one property name: String
var listData = [];
hierarchyData.forEach(function(item) {
item.subItems.forEach(function(subItem) {
// Here is the key for getting sticky headers
// Make sure the header title is set for each individual item
subItem.header = item.name;
listData.push(subItem);
});
});
function createItemDiv() {
var d = document.createElement('div');
d.className = 'list-group moving-div';
var u = document.createElement('ul');
d.appendChild(u);
return { d, u };
}
function onVirtualBeforeInsert(vlist, fragment) {
var self = this;
var list = Dom7(vlist.ul);
var virtualItems = [];
var first = true;
var top;
while (fragment.hasChildNodes()) {
var c = fragment.removeChild(fragment.firstChild);
if (first) {
first = false;
top = c.style.top;
}
Dom7(c).css('top', '');
virtualItems.push(c);
}
var previousName = null;
var addedHeaders = [];
var e;
virtualItems.forEach(item => {
var headerName = Dom7(item).attr('data-header-name');
if (previousName !== headerName) {
previousName = headerName;
e = createItemDiv();
fragment.appendChild(e.d);
Dom7(e.d).css('top', top);
var h = document.createElement('li');
h.className = 'list-group-title';
h.appendChild(document.createTextNode(headerName));
e.u.appendChild(h);
}
e.u.appendChild(item);
});
vlist.setListSize();
list.css({ height: `${list.height() + ((app.theme == 'ios' ? 31 : 48) * addedHeaders.length)}px` });
}
var virtualList = app.virtualList.create({
el: '.virtual-list',
items: listData,
height: app.theme === 'ios' ? 44 : 54,
// Make each item's header available in the DOM for easier code
itemTemplate: '<li data-header-name="{{header}}"><a href="#">{{name}}</a></li>',
on: {
itemsBeforeInsert: onVirtualBeforeInsert
}
});