How to make custom table in custom element?

Viewed 1725

I am working on a custom element with lit-element. I want to make my own custom table element, but there are some problem.

First. It seemed browser allow tr, td tags for only table.

<!-- in html -->
<my-table>
  <tr>
    <td>A</td>
    <td>B</td>
  </tr>
</my-table>

<!-- chrome rendered -->
<my-table> A B </my-table>

Second. It could imitate with CSS, but CSS can't implement colspan.

<my-table> <!-- display: table -->
  <my-tr> <!-- display: table-row -->
    <my-td>A</my-td> <!-- display: table-cell -->
    <my-td colspan="3">B</my-td> <!-- There is no way to implement colspan -->
  </my-tr>
</my-table>

Third. I try to style with 'display: content' on host. It seemed to be work, but host has no data for box-sizing like clientHeight.

class MyTd extends LitElement {
  render() {
    return html`
      <style>
        :host { display: content; }
        td {
          all: inherit; /* It make to inherit host's style */
          display: table-cell;
        }
      </style>
      <td>
        ${html`<slot></slot>`} // I don't know why it work.
      </td>
    `;
  }
}

// other js file.
const td = document.querySelector('my-td');
td.clientHeight; // 0

I can override clientHeight and something else (offsetHeight, getBoundingClientRect...) but I don't know it is proper and it is only way.

Is there any other way to create custom table or there is something wrong in my thinking?

1 Answers

This isn't really a good use case for a custom element - the question is why would I use <my-td> when <td> exists and works everywhere?

You can use column-count in CSS to imitate tables, but you have more severe issues - browsers (and related a11y tools) see the DOM for <table> as a structured table of data, while your <my-table> is just another custom element.

This breaks the DOM hierarchy for the table too - even though your elements wrap the related table elements they don't inherit or extend them.

So for your first example your DOM is something like this:

<my-table>
  #shadow-root
    <tr>
      <td>A</td>
      <td>B</td>
    </tr>
</my-table>

Nothing the browser usually does to hook up cells to rows and rows to tables will work across those shadow DOM boundaries, and the <tr> is rendered exactly as if it was at the root of a new document fragment.

Likewise your DOM for the third attempt looks something like this:

<my-table>
  #shadow-root
    <table>
       <my-tr>
         #shadow-root
           <tr>
             <my-td>
               #shadow-root
                 <td>

Each of those shadow roots is a new isolated fragment.

The question becomes what do you want to do with this structure?

Do you want something that looks like a table but uses custom elements for rows and cells consider using display: grid layouts instead. It's quite a bit more powerful than tables and in custom elements the CSS is much better supported than the rather inconsistent display: table-cell.

If you want something structured as a table consider putting the entire table in the shadow root and passing the data as a property

@customElement('my-table')
class MyTable 
  extends LitElement {
  render() {
    return html`
<table>
  ${this.items.map(i => html`
  <tr>
    <td>A</td>
    <td>B</td>
  </tr>`)}
</table>
    `;
  }

  @property({attribute: false})
  items: readonly RowRecordType[];
}

Then call this with a property prefix:

<my-table .items=${theListOfRows}></my-table>

Finally, if you want to extend <td> you can add to it with the is= syntax, but this was a bit of a dead end on the spec, isn't supported at all on Safari or Opera and is flakey on the rest.

You can extend a cell:

class MyCell
    extends HTMLTableCellElement { ...

customElements.define('my-cell', MyCell, { extends: 'td' });

And then do:

<table>
  <tr>
    <td is="my-cell">A</td>
    <td is="my-cell">B</td>
  </tr>
</table>

However, this isn't supported by LitElement and you're better off implementing your custom web component directly.

Related