20

I'm trying to build a LWC that renders a complex table-based UI. It needs to have distinct rows and columns with dynamically-determined colspans - a real table. It's for assigning lodging, and each row will represent a room and each column for which the room can be assigned. It will involve drag/drop etc.

I was hoping to make each row and each cell a component, so that data and logic could be encapsulated in each of those. However, that doesn't seem to work because LWC includes a tag w/ the component name in the rendered HTML.

Here's some test code.

Table component markup:

<template>
<p>
    This is our very nice room table!
</p>
<table style="border:1px black solid">
    <!--header row goes here -->
    <template for:each={rooms} for:item="room">
        <c-ncilodgeroomtablerow key={room}>
        </c-ncilodgeroomtablerow>
    </template>
</table>
</template>

The var rooms is just set to [1,2,3] in the js controller so that it will iterate 3 times.

Here's the row component markup, just hard-coded w/ static content for now:

<template>
<tr>
    <td>
        cell one
    </td>
    <td>
        cell two
    </td>
</tr>
</template>

This does not render as desired. It ends up with everything in one cell, because it puts c- tags around the tr tags, and I think the tr's need to be immediate children of the table. Here's the rendered markup:

<table style="border: 1px solid black;">
<c-ncilodgeroomtablerow>
    <tr>
        <td>cell one</td>
        <td>cell two</td>
    </tr>
</c-ncilodgeroomtablerow>
<c-ncilodgeroomtablerow>
    <tr>
        <td>cell one</td>
        <td>cell two</td>
    </tr>
</c-ncilodgeroomtablerow>
<c-ncilodgeroomtablerow>
    <tr>
        <td>cell one</td>
        <td>cell two</td>
    </tr>
</c-ncilodgeroomtablerow>
</table>

I also tried putting the trs in the table component markup, inside the iterator, and then changing the row component to just include the td tags and not the tr's. That didn't work either - again because of the c- tags inside the tr's.

Is there any way to make something like this work w/ LWCs? Or do I need to look into making a css-based table layout? Open to general advice.

2

4 Answers 4

25

Building on the answer provided already by @sfdcfox, this following seems to be working for me and allows me to componentize the entire row (much easier to emit events this way).

parent.html

<table class="slds-table slds-table_bordered slds-border_left slds-border_right">
  <thead>
    <tr class="slds-line-height_reset">
      <th class="" scope="col">
        <div class="slds-truncate" title="Name">Name</div>
      </th>
      <th class="" scope="col">
        <div class="slds-truncate" title="Some Custom Column">Some Custom Column</div>
      </th>
    </tr>
  </thead>
  <tbody>
    <template for:each={iterableDataSet} for:item="row">
      <c-child-row row={row}>
      </c-child-row>
    </template>
  </tbody>
</table>

childRow.html

<template>
  <!-- taken straight from SLDS -->
  <th data-label="Name" scope="row">
    <div class="slds-truncate" title={row.Name}>{row.Name}</div>
  </th>
  <td data-label="Some Custom Column">
    <div class="slds-truncate">
      <lightning-input
        data-id={row.Id}
        type="number"
        variant="label-hidden"
        name="inputForDataField"
        onchange={handleOnChange}
        value={row.Data_Field__c}>
      </lightning-input>
    </div>
  </td>
  <!-- more tds .... -->
</template>

And the critical piece on childRow.css

:host {
  display: table-row;
}
4
  • Finally got a chance to try this, and worked a treat! Thanks @tsalb
    – mscholtz
    Mar 5, 2019 at 1:42
  • 3
    Wouldn't adding display: contents to the row work too? It would skip over the parent element and therefore give the table proper HTML table structure? Feb 4, 2020 at 1:28
  • @GusMelendez that works! IMHO the better solution as it keeps the HTML table structure intact, as you said. In my case it also allowed me to render a td or a th conditionally in a childCell component which otherwise would be a hassle
    – Steven.B
    Jan 6, 2021 at 11:22
  • What's the iterableDataSet? How can the .html know the number of the rows and columns to make the loop correctly?
    – Cline
    Jan 18, 2022 at 22:08
10

Ideally, you shouldn't use table; lightning-layout and lightning-layout-item work as tables for all practical purposes. If that doesn't work for you, then you'd want to simply apply the appropriate CSS to your components.

Row Display

:host {
  display: table-row;
}

Cell Display

:host {
  display: table-cell;
}

I seem to be having a problem writing a working version of this, but it should work once everything is nested. Do not include tr in your row component, nor td in your cell component. Your component becomes the row/cell, so you don't need to nest them.

6
  • Thanks @sfdcfox, we thought of that but I don't believe that the CSS version of the table allows you to do arbitrary colspans. i.e. you can make a row into a single cell that spans the whole row, but you can't make 1 cell in the middle of a row span two columns. AFAIK. And that's something we need.
    – mscholtz
    Feb 21, 2019 at 5:42
  • 1
    Hmm, can't you just use a slds-grid? <div class="slds-grid slds-gutters"> <div class="slds-col slds-size_2-of-12">... etc Feb 21, 2019 at 7:10
  • @Casper, oh, I had forgotten about that. Good suggestion. In this case, though, we actually want things like flush gridlines etc. i.e. we really want it to look like a traditional table. And we will need to support more than 12 columns, which slds grid does not.
    – mscholtz
    Feb 21, 2019 at 7:50
  • Our tentative direction is to not make the rows and cells components. Instead the table component is rendering the trs and tds in iterators and then putting components inside the cells. This works, but means that we will have to do rerendering at the whole tabel level instead of further down the DOM. We are seeing some other minor artifacts of having those <c-...> tags in the markup, including some CSS oddness. Anyone know why those need to be in there?
    – mscholtz
    Feb 21, 2019 at 7:53
  • @mscholtz You'll want to read about the shadow DOM. Each LWC becomes a shadow root, isolating CSS and internal DOM structure into a custom HTML element. This means that you can only style the outer part of the component, as if it were a real DOM element (because it is) instead of as an entire DOM tree. The <c-...> elements are there because that's how shadow DOM works.
    – sfdcfox
    Feb 21, 2019 at 11:04
6

I agree with most statements from the comments where there are better choices than a table but another option I found is to use the following CSS in your "row" component:

:host{
    display: contents;
}

This causes an element's children to appear as if they were direct children of the element's parent, ignoring the element itself.

Do note that browser support still has some issues.

https://caniuse.com/#search=display%3A%20contents

Specifically in the accessibility area (people using assistive technology). You can read about that here:

https://hiddedevries.nl/en/blog/2018-04-21-more-accessible-markup-with-display-contents

So if accessibility would be a criteria for your components, keep this in mind.

1

What may work is dummy slots that pass data into the parent via eventDispatch like the lightning-column and lightning-columns work.

<template>
    <c-table>
        <c-table-heading>
            <c-table-cell>id</c-table-cell>
            <c-table-cell>name</c-table-cell>
        </c-table-heading>

        <c-table-row>
            <c-table-cell>1</c-table-cell>
            <c-table-cell>Bob</c-table-cell>
        </c-tabel-row>
    </c-table>
</template>

c-table-row and c-table-heading (could just be one component with an @api for heading)

class TableRow extends LightningElement {
    get slotElement() {
        return this.template.querySelector('slot');
    }

    get slotChildren() {
        return this.slotElement.children;
    }

    renderedCallback() {
        this.dispatchEvent(
            new CustomEvent('privatetablerow', {
                detail: { slot: this.slotChildren },
            })
        );
    }
}

c-table...

class Table extends LightningElement {
    connectedCallback() {
        this.privatelistener = (event) => {
            event.stopPropagation();
            const { slot } = event.detail;
            
            /**
             * From here you have access to the children passed into <t-table-cell>.
             * You can add your slots to an array and create a loop making <tr> items.
             * 
             * Of note: you should keep track of the nodes and overwrite them in the array.
             * I will give all my elements a unique id and pass that into the detail object.
             * I do this by extending LightningElement into a CustomElement and having all 
             * my elements extend CustomElement instead of LightningElement.
             */
        };

        this.addEventListener('privatetablerow', this.privatelistener);
    }

    disconnectedCallback() {
        this.removeEventListener('privatetablerow', this.privatelistener);
    }
}

This would be a bare minimum approach. You would still need to build the template for c-table and using lwc:dom="manual" inject createElement('tr') and 'td' elements.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .