I’ve created a custom element that mimics c#’s datagridview. I was able to make the <thead> stick at the top like dot net’s listview or datagridview header until just recently I noticed it doesn’t work anymore.
Everything in this custom element are programmatically generated including the CSS rules. Here’s the code to generate the CSS rules:
class ListView extends HTMLElement {
static #lvwId = null;
static #lvwCss;
static #initId() {
if (ListView.#lvwId == null) {
ListView.#lvwId = '-lvw-' + Array.from(Date.now().toString()).map(b => ('00' + b.toString(16)).slice(-2)).join('');
ListView.#lvwCss = ListView.#lvwId + '-css';
}
}
connectedCallback() {
ListView.#initId();
let style = document.getElementById(ListView.#lvwId);
if (style == null) {
ListView.#style = style = document.createElement('style');
style.type = 'text/css';
style.id = ListView.#lvwId;
const fn = (...b) => {
if (b.length == 1) {
let c = b[0];
let d = '.' + ListView.#lvwCss;
if (c[0] == '-')
c = c.substr(1);
else
d += ' ';
d += c;
ListView.#style.appendChild(document.createTextNode(d));
}
else {
let c = '';
for (let d of b[0]) {
if (c != '') c += ',';
c += '.' + ListView.#lvwCss + ' ' + d;
}
ListView.#style.appendChild(document.createTextNode(c + ' ' + b[1]));
}
};
fn('{position:relative;background-color:white;outline:none;display:block;overflow:auto;}');
fn('*{user-select:none;box-sizing:border-box;position:relative;padding:0px;margin:0px;background-color:transparent;color:inherit}');
fn('{--gridcolor:lightgray;--xmargins:5px;--itembackcolor:white;--itemforecolor:black;--headerbackcolor:gray;--headerforecolor:black;--headerhoverback:forestgreen;--headerhoverfore:black;--headerpadding:4px;--itempadding:4px;}');
fn('table{border-collapse:collapse;z-index:0;left:0px;top:0px;background:white;table-layout:fixed;display:block}');
fn('thead{position:sticky;top:0px;z-index:3}');
fn('thead>tr{position:sticky}');
fn(['th', 'td'], '{text-align:left;vertical-align:top;}');
fn('th{position:sticky;top:-1px;background-color:var(--headerbackcolor);color:var(--headerforecolor);border-right:1px solid var(--gridcolor);transition:background-color .5s}');
fn('-:not([headeronly]) th{cursor:pointer;}');
fn('-:not([headeronly]) th:hover{background-color:var(--headerhoverback);color:var(--headerhoverfore)}');
fn(['th>div', 'td>div'], '{height:100%;display:flex;flex-direction:row;justify-content:flex-start;align-content:flex-start;align-items:flex-start}');
fn('th>div{margin-left:var(--xmargins);padding: var(--headerpadding) var(--xmargins) var(--headerpadding) 0px;}');
fn('th>div>p{width:100%}');
fn('td{background:var(--itembackcolor);color:var(--itemforecolor);}')
fn('th::after{content:"";width:var(--xmargins);position:absolute;right:0px;top:0px;height:100%;cursor:ew-resize}')
fn('th:not(:first-child)::before{content:"";width:var(--xmargins);position:absolute;left:0px;top:0px;height:100%;cursor:ew-resize}')
fn('td>div{padding:var(--xmargins) var(--itempadding);width:100%;}');
fn('td>div>p>img{float:left;margin-right:5px}');
fn('td>input{height:100%;width:100%;outline:0px solid transparent;border:none;padding:1px;background:white;color:black;}');
fn('td{color:black}');
fn('tr[selected]{background:#018;color:white}');
fn('tr[selected]>td:not([selected]){background:transparent;color:white}');
fn('tr[selected]>td[selected]{background:rgba(255,255,255,.2);}');
fn('tbody>tr:nth-child(even){--bg-color:rgba(0,0,0,.05)}');
fn('tbody>tr:nth-child(even) td{background-color:var(--bg-color);}');
fn('.sticky-hdr{position:sticky;overflow:hidden;z-index:1;top:0px;width:100%}');
fn('tbody>tr{border-bottom:1px solid var(--gridcolor);color:black}');
fn('.splits{border-top:1px solid var(--gridcolor)}');
fn('tbody>tr>td p{outline:none;width:100%;}');
fn('tbody td{border-right:1px solid var(--gridcolor)}');
fn('-[nogrid] tbody * {border-color:transparent}');
fn('-[nogrid] tbody>tr:nth-child(even)>td{border-color:var(--bg-color)}');
fn('-[nowrap] p{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}');
fn('-[checkboxes] td>div>div[checkbox]{float:left;border:1px solid gray;margin-right:5px;width:16px;height:16px;background:white}')
fn("-[checkboxes] td>div>div[checkbox="checked"]::after{content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' x='0' y='0' viewbow='0 0 16 16'%3E%3Cpath fill='black' d='M4,9l0,-3l3,3l5,-5,l0,3l-5,5Z' /%3E%3C/svg%3E");width:16px;height:16px;position:absolute;left:-1.5px;top:-1px}");
document.body.insertAdjacentElement('beforebegin', style);
}
this.tabIndex = $(this).index();
$(this).addClass(ListView.#lvwCss);
this.#headerPlaceholder = document.createElement('div');
this.#headerPlaceholder.setAttribute('style', 'width:100%;height:32px;overflow:none;position:sticky;left:0px;top:0px;background:gray');
this.#headerPlaceholder.innerHTML = ' ';
this.append(this.#headerPlaceholder);
this.#table = document.createElement('table');
this.#table.style.top = '0px';
this.#thead = document.createElement('thead');
this.#table.appendChild(this.#thead);
this.#tbody = document.createElement('tbody');
this.#table.appendChild(this.#tbody);
this.#header = document.createElement('tr');
this.#table.firstChild.insertAdjacentElement('afterbegin', this.#header);
$(this).append(this.#table);
this.#handleEvents();
this.#handleHeaderEvents();
const observer = new ResizeObserver(entries => {
const ofh = this.#thead.offsetHeight - 1;
this.#headerPlaceholder.style.height = ofh + 'px';
this.#table.style.top = (-ofh) + 'px';
});
observer.observe(this.#thead);
}
}
Inside the connectedCallback function is the creation of this.#headerPlaceholder:
this.#headerPlaceholder = document.createElement('div');
this.#headerPlaceholder.setAttribute('style', 'width:100%;height:32px;overflow:none;position:sticky;left:0px;top:0px;background:gray');
this.#headerPlaceholder.innerHTML = ' ';
And as you can see, this element is sticky and it do sticks. But it’s the <thead> that sticky positioning don’t work. Again, it was working and I don’t know why it just suddenly stopped working.
I have searched a lot to find the cause of this but I’ve got no clue. I followed the suggested solutions and accepted solutions I’ve found in stackoverflow like combining sticky position with css top rule but still it doesn’t work. I’ve tried using position fixed but it created another problem of column sizes e.g. <th> element not following the width value and header column widths and td widths not the same. I’ve checked if outside rules are affecting but nothing I’ve found so far. I have also extended the sticky position to all <tr>s and <th>s but to no avail.
A non-jquery solution is preferrable although you might observe that there are some jquery calls in the snippet I have posted. I am trying my custom element to have less to zero dependencies but as of now it uses jquery in some parts.
Update
I can’t directly paste the generated CSS and mark-up here but I think links will do:
Generated CSS: https://pastebin.com/embed_js/9tVx409n
Generated mark-up: https://pastebin.com/embed_js/6EmpvY6Q
Edit: adding an SO snippet:
<div class="flex fdcol jcsb cx100 cy100">
<div class="cx100 cy100">
<ek-listview class="cx100 -lvw-01070108000005050008030200-css" id="lvw" nowrap="" style="border: 1px solid rgb(170, 170, 170); width: 1094px; height: 265.672px;" tabindex="0">
<table style="top: 0px; width: 890px;">
<thead>
<tr>
<th style="width: 250px;">
<div>
<p style="text-align: left;">Name</p>
</div>
</th>
<th style="width: 100px;">
<div>
<p style="text-align: left;">Bio Name</p>
</div>
</th>
<th style="width: 100px;">
<div>
<p style="text-align: left;">Date Hired</p>
</div>
</th>
<th style="width: 120px;">
<div>
<p style="text-align: left;">Service Length</p>
</div>
</th>
<th style="width: 120px;">
<div>
<p style="text-align: left;">Position</p>
</div>
</th>
<th style="width: 90px;">
<div>
<p style="text-align: right;">Salary</p>
</div>
</th>
<th style="width: 110px;">
<div>
<p style="text-align: right;">Payments</p>
</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div>
<p><span>Employee 1</span></p>
</div>
</td>
<td>
<div>
<p><span></span></p>
</div>
</td>
<td>
<div>
<p><span>Aug 1, 2019</span></p>
</div>
</td>
<td>
<div>
<p><span>4 years, 10 months & 9 days</span></p>
</div>
</td>
<td>
<div>
<p><span>Sales Executive</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span>9,000.00</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span></span></p>
</div>
</td>
</tr>
<tr>
<td>
<div>
<p><span>Employee 2</span></p>
</div>
</td>
<td>
<div>
<p><span></span></p>
</div>
</td>
<td>
<div>
<p><span>May 3, 2023</span></p>
</div>
</td>
<td>
<div>
<p><span>1 year, 1 month & 7 days</span></p>
</div>
</td>
<td>
<div>
<p><span>Installer</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span>9,100.00</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span></span></p>
</div>
</td>
</tr>
<tr>
<td>
<div>
<p><span>Employee 4</span></p>
</div>
</td>
<td>
<div>
<p><span></span></p>
</div>
</td>
<td>
<div>
<p><span>Dec 7, 2021</span></p>
</div>
</td>
<td>
<div>
<p><span>2 years, 6 months & 3 days</span></p>
</div>
</td>
<td>
<div>
<p><span>Installer</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span>10,400.00</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span></span></p>
</div>
</td>
</tr>
<tr>
<td>
<div>
<p><span>Employee 5</span></p>
</div>
</td>
<td>
<div>
<p><span></span></p>
</div>
</td>
<td>
<div>
<p><span>May 1, 2023</span></p>
</div>
</td>
<td>
<div>
<p><span>1 year, 1 month & 8 days</span></p>
</div>
</td>
<td>
<div>
<p><span>Installer</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span>9,999.00</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span></span></p>
</div>
</td>
</tr>
<tr>
<td>
<div>
<p><span>Employee 6</span></p>
</div>
</td>
<td>
<div>
<p><span></span></p>
</div>
</td>
<td>
<div>
<p><span>Apr 4, 2024</span></p>
</div>
</td>
<td>
<div>
<p><span>0 year, 2 months & 6 days</span></p>
</div>
</td>
<td>
<div>
<p><span>Office Admin</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span>9,750.00</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span></span></p>
</div>
</td>
</tr>
<tr>
<td>
<div>
<p><span>Employee 7</span></p>
</div>
</td>
<td>
<div>
<p><span></span></p>
</div>
</td>
<td>
<div>
<p><span>Mar 1, 2024</span></p>
</div>
</td>
<td>
<div>
<p><span>0 year, 3 months & 9 days</span></p>
</div>
</td>
<td>
<div>
<p><span>Installer</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span>10,444.00</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span></span></p>
</div>
</td>
</tr>
<tr>
<td>
<div>
<p><span>Employee 8</span></p>
</div>
</td>
<td>
<div>
<p><span></span></p>
</div>
</td>
<td>
<div>
<p><span>Apr 3, 2024</span></p>
</div>
</td>
<td>
<div>
<p><span>0 year, 2 months & 6 days</span></p>
</div>
</td>
<td>
<div>
<p><span>Installer</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span>9,750.00</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span></span></p>
</div>
</td>
</tr>
<tr>
<td>
<div>
<p><span>Employee 9</span></p>
</div>
</td>
<td>
<div>
<p><span></span></p>
</div>
</td>
<td>
<div>
<p><span>Sep 11, 2023</span></p>
</div>
</td>
<td>
<div>
<p><span>0 year, 8 months & 29 days</span></p>
</div>
</td>
<td>
<div>
<p><span>Area Supervisor</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span>9,750.00</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span></span></p>
</div>
</td>
</tr>
<tr>
<td>
<div>
<p><span>Employee 10</span></p>
</div>
</td>
<td>
<div>
<p><span></span></p>
</div>
</td>
<td>
<div>
<p><span>Feb 1, 2024</span></p>
</div>
</td>
<td>
<div>
<p><span>0 year, 4 months & 9 days</span></p>
</div>
</td>
<td>
<div>
<p><span>IT Programmer</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span>12,000.00</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span></span></p>
</div>
</td>
</tr>
<tr>
<td>
<div>
<p><span>Employee 11</span></p>
</div>
</td>
<td>
<div>
<p><span></span></p>
</div>
</td>
<td>
<div>
<p><span>Feb 26, 2024</span></p>
</div>
</td>
<td>
<div>
<p><span>0 year, 3 months & 13 days</span></p>
</div>
</td>
<td>
<div>
<p><span>Office Admin</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span>9,750.00</span></p>
</div>
</td>
<td>
<div>
<p style="text-align: right;"><span></span></p>
</div>
</td>
</tr>
</tbody>
</table>
</ek-listview>
</div>
</div>
<style type="text/css" id="-lvw-01070107090900090401010003">
.-lvw-01070107090900090401010003-css {
position: relative;
background-color: white;
outline: none;
display: block;
overflow: auto;
}
.-lvw-01070107090900090401010003-css * {
user-select: none;
box-sizing: border-box;
position: relative;
padding: 0px;
margin: 0px;
background-color: transparent;
color: inherit
}
.-lvw-01070107090900090401010003-css {
--gridcolor: lightgray;
--xmargins: 5px;
--itembackcolor: white;
--itemforecolor: black;
--headerbackcolor: gray;
--headerforecolor: black;
--headerhoverback: forestgreen;
--headerhoverfore: black;
--headerpadding: 4px;
--itempadding: 4px;
}
.-lvw-01070107090900090401010003-css table {
border-collapse: collapse;
z-index: 0;
left: 0px;
top: 0px;
background: white;
table-layout: fixed;
display: block
}
.-lvw-01070107090900090401010003-css thead {
position: sticky;
z-index: 11;
top: 0px
}
.-lvw-01070107090900090401010003-css th,
.-lvw-01070107090900090401010003-css td {
text-align: left;
vertical-align: top;
}
.-lvw-01070107090900090401010003-css th {
position: sticky;
top: 0px;
background-color: var(--headerbackcolor);
color: var(--headerforecolor);
border-right: 1px solid var(--gridcolor);
transition: background-color .5s
}
.-lvw-01070107090900090401010003-css:not([headeronly]) th {
cursor: pointer;
}
.-lvw-01070107090900090401010003-css:not([headeronly]) th:hover {
background-color: var(--headerhoverback);
color: var(--headerhoverfore)
}
.-lvw-01070107090900090401010003-css th>div,
.-lvw-01070107090900090401010003-css td>div {
height: 100%;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-content: flex-start;
align-items: flex-start
}
.-lvw-01070107090900090401010003-css th>div {
margin-left: var(--xmargins);
padding: var(--headerpadding) var(--xmargins) var(--headerpadding) 0px;
}
.-lvw-01070107090900090401010003-css th>div>p {
width: 100%
}
.-lvw-01070107090900090401010003-css td {
background: var(--itembackcolor);
color: var(--itemforecolor);
}
.-lvw-01070107090900090401010003-css th::after {
content: "";
width: var(--xmargins);
position: absolute;
right: 0px;
top: 0px;
height: 100%;
cursor: ew-resize
}
.-lvw-01070107090900090401010003-css th:not(:first-child)::before {
content: "";
width: var(--xmargins);
position: absolute;
left: 0px;
top: 0px;
height: 100%;
cursor: ew-resize
}
.-lvw-01070107090900090401010003-css td>div {
padding: var(--xmargins) var(--itempadding);
width: 100%;
}
.-lvw-01070107090900090401010003-css td>div>p>img {
float: left;
margin-right: 5px
}
.-lvw-01070107090900090401010003-css td>input {
height: 100%;
width: 100%;
outline: 0px solid transparent;
border: none;
padding: 1px;
background: white;
color: black;
}
.-lvw-01070107090900090401010003-css td {
color: black
}
.-lvw-01070107090900090401010003-css tr[selected] {
background: #018;
color: white
}
.-lvw-01070107090900090401010003-css tr[selected]>td:not([selected]) {
background: transparent;
color: white
}
.-lvw-01070107090900090401010003-css tr[selected]>td[selected] {
background: rgba(255, 255, 255, .2);
}
.-lvw-01070107090900090401010003-css tbody>tr:nth-child(even) {
--bg-color: rgba(0, 0, 0, .05)
}
.-lvw-01070107090900090401010003-css tbody>tr:nth-child(even) td {
background-color: var(--bg-color);
}
.-lvw-01070107090900090401010003-css tbody>tr {
border-bottom: 1px solid var(--gridcolor);
color: black
}
.-lvw-01070107090900090401010003-css .splits {
border-top: 1px solid var(--gridcolor)
}
.-lvw-01070107090900090401010003-css tbody>tr>td p {
outline: none;
width: 100%;
}
.-lvw-01070107090900090401010003-css tbody td {
border-right: 1px solid var(--gridcolor)
}
.-lvw-01070107090900090401010003-css[nogrid] tbody * {
border-color: transparent
}
.-lvw-01070107090900090401010003-css[nogrid] tbody>tr:nth-child(even)>td {
border-color: var(--bg-color)
}
.-lvw-01070107090900090401010003-css[nowrap] p {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
.-lvw-01070107090900090401010003-css[checkboxes] td>div>div[checkbox] {
float: left;
border: 1px solid gray;
margin-right: 5px;
width: 16px;
height: 16px;
background: white
}
.-lvw-01070107090900090401010003-css[checkboxes] td>div>div[checkbox="checked"]::after {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' x='0' y='0' viewbow='0 0 16 16'%3E%3Cpath fill='black' d='M4,9l0,-3l3,3l5,-5,l0,3l-5,5Z' /%3E%3C/svg%3E");
width: 16px;
height: 16px;
position: absolute;
left: -1.5px;
top: -1px
}
</style>
2
Answers
First of all, guys, thank you for your time. In the widget or component or whatever it may be appropriately called, my main objective is to have a resizable header that sticks at the top. Whatever I did I just can’t make it happen. I tried to hardcode the enclosing div’s width and height but it still didn’t solve the problem.
I want this component to be as reusable as possible (at least, for me) without having to do other stuff aside from adding it to a web page. I mean, no more linking to external css and js files.
So, my solution, although I think not a permanent one but at least it serves the purpose, is to have two tables. The first will serve as the header and the other is the body.
Both tables are inside a div for the scrollbars.
The basic css goes like this:
The dynamic creation of css is necessary to create an ID as unique as possible for the style element. The purpose is to avoid collition with other elements’ IDs. The generated ID concatenated with the string '-css' will serve as the root class of the component to make all css rules scoped (I am not sure if it is the right term). This ID also ensures that only style is created for all instances of the component.
To ascertain the creation of the unique ID, this method is called
And in the
connectedCallback()
function...Note that there is a
fn()
method inside theconnectedCallback()
method which obvious is responsible in placing the css rules. It also ensures that all rules are prepended with theListView.#lvwCss
value.Regarding the ResizeObserver inside the connectedCallback, it is necessary to make sure that the position of the second table is right at the bottom of the first table. The header table height could change, first: when nowrap attribute is not specified when adding the component in the web page and, second: where there are split columns.
The basic mark-up could look like this:
The
<colgroup>
’s main purpose is the synchronized resizing of columns of both tables. The component is fully customizable through the css variables provided and of course its design can still be manipulated through the last css rules or inline styles.Since adding headers and rows are done via script, here is a sample script.
And adding items goes like this:
Again, thank you.
The class of the ek-listview ancestor does not match all the classes of the ancestors in the CSS.
The CSS has .-lvw-01070107090900090401010003-css
The ek-listview has class -lvw-01070108000005050008030200-css
Changing the class (by hand) in the snippet gives formatting of the table, with the thead having position sticky.
We can see the class being created dynamically here:
It is dependent on date so I think what has happened is that the CSS being invoked is not in step with the dynamic code. They have to be created at the same time.