[SalesForce] Dynamically accessing iteration item properties in a Template iteration on lightning web component

I would enjoy using the easy to set up lightning-datatable, but it just lacks any ability to really customize it. It would work, but I need text-wrapping in cells instead of truncation. And it lets the user do that with the drop down, but we have these tables in visualforce pages inside apex:forms, which causes the entire page to reload if you click the little arrow in the header to change from Clip text to Wrap text….

So, I am going for a custom html table with slds styling. This is what I am attempting to do:

<table class="slds-table slds-table_bordered slds-table_striped">
    <thead>
        <tr class="slds-line-height_reset">
            <template for:each={tableColumns} for:item="column"><!--Need the LWC html repeat-->
                <th key={column.fieldName} class="slds-text-title_caps slds-cell-wrap" scope="col">{criterion.label}</th>
            </template>
        </tr>
    </thead>
    <tbody>
        <template for:each={tableData} for:item="criterion" for:index="index">
            <tr key={criterion.id}>
                <td>{index}</td>
                <template for:each={tableColumns} for:item="column">
                    <td key={}>{criterion[column.fieldName]}</td>
                </template>
            </tr>
        </template>
    </tbody>
</table>

My question comes with the <td key={}>{criterion[column.fieldName]}</td>.

In order to prevent any kind of hard coding of table columns or cells, doing the double iteration would be nice, but I'll need to dynamically access the value of criterion. Is such a thing possible? Is there any way to do this? It doesn't seem to like how I have it at the moment (I know key is missing still).

Best Answer

I have assumed, that data in criterion will be in this format.

{
    field1: "value"
    field2:223,
    ...
    ...
}

As we can't use expressions in LWC HTML, you need to preprocess the table data into an array. Basically a two-dimensional array. Instead of having each row as an object, I changed it to an array of key, fieldApiName, and value because we need generic field identifiers.

let criterionDataArray = [];
this.criterionArray.forEach((criterionRow, index) => {
    let newRow = Object.keys(criterionRow).map((currentValue, key) => {
        return {
            fieldApiName: key,
            value: currentValue,
            key: `${index}${key}`
        }
    });
    criterionDataArray.push({ Id: index, data: newRow });
});
this.tableData = criterionDataArray;

This is how you can iterate that.

HTML

<template if:true={tableData} for:each={tableData} for:item="criterion" for:index="index">
    <tr key={criterion.Id}>
        <td>{index}</td>
        <template if:true={criterion.data} for:each={criterion.data} for:item="row">
            <td data-field-api-name={row.fieldApiName} key={row.key}>{row.value}</td>
        </template>
    </tr>
</template>

When you need to save the data back, you need to convert this into an original format. You can do the same using array.map method.