Fixed table layout and a single row w/ pseudo element leading to shrunk table width

Viewed 39

I've a table in my project with a pseudo element to show which row is active. Having changed the table layout to fixed (which is needed), I started getting this strange layout where the active row would expand to take up the entire table, but the other rows would not:

Table width issue

I've replicated a similar problem here (codepen, snippet below) - it's not exactly the same (the active row doesn't extend), but I'm fairly sure any answer to this would help me fix my problem.

If you comment out the top active::after style you'll see the table return to its correct size.

Thanks

// TABLE DATA
const headers = ['Id', 'Name', 'Age', 'Location'];

const datasetOne = [
  ['1','John Jones','27','Swindon'],
  ['2', 'Pete Best', '23', 'Glasgow'],
  ['3', 'Jules West', '22', 'Exeter'],
  ['4', 'Kate Ford', '33', 'Fife'],
];
const datasetTwo = [
  ['5','Ruth Thompson','27','Birmingham'],
  ['6', 'Dominic Lawson', '23', 'Greater London'],
  ['7', 'Really really long name', '22', 'Manchester'],
  ['8', 'Nicholas Johnson', '33', 'Liverpool'],
];


const tableWrapper = document.querySelector('.table-wrapper');
const btn = document.querySelector('.btn');
let dataset = 1;

// Listeners
window.addEventListener('load', () => {
  const data = formatData(datasetOne);
  tableWrapper.insertAdjacentHTML('afterbegin', createTable(headers, data));
});
btn.addEventListener('click', () => {
  // Remove the table
  const table = document.querySelector('.table')
  table.parentElement.removeChild(table);

  // Create and insert a new table
  let data;
  if(dataset === 1) {
    data = formatData(datasetTwo);
    dataset = 2;
  }
  else if(dataset === 2) {
    data = formatData(datasetOne);
    dataset = 1;
  }
  tableWrapper.insertAdjacentHTML('afterbegin', createTable(headers, data));
  
})

// Functions to create the table
function formatData(data) {
    const rows = data.map(row => {
        return createHTMLRow(row);
    });
    return rows;
}
function createHTMLRow([id, name, age, location]) {
    const row = [
        `<td class="td--id">${id}</td>`,
        `<td class="td--name">${name}</td>`,
        `<td class="td--age">${age}</td>`,
        `<td class="td--location">${location}</td>`
    ];
    return row;
}
function createTable (theads, rows) {
    const markup = `
        <table class="table">
            <thead class="thead">
                <tr>
                    ${theads.map((thead) => {
                        return `<th class="th--${thead.toLowerCase()}">${thead}</th>`;
                    }).join('')} 
                </tr>
            </thead>
            <tbody class="tbody">
                ${
                    rows.map((row, index) => {
                        return `<tr class="row ${index===0? 'active':''}">${row.map(col => {
                            return `${col}`
                        }).join('')}</tr>`
                    }).join('')
                }
            </tbody>
        </table>
    `;
    return markup;
};
.active::after {
  position: absolute;
  content: '';
  left: 0;
  top: 0;
  width: 2px;
  height: 100%;
  background-color: green;
}

* {
  margin: 0;
  box-sizing: border-box;
}

.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100vh;
  background-color: firebrick;
}


.table-wrapper {
  display: flex;
  background-color: white;
  width: 30rem;
  overflow: hidden;
}

.table {
  display: table;
  table-layout:fixed;
  border-collapse: collapse;
  overflow: hidden;
  width: 100%;
}

th {
  text-align: start;
  padding: 1rem;
  background-color: lemonchiffon;
  border: 1px solid lightgrey;
}
.th--age, .th--id {
  width: 4rem;
}

td {
  padding: .5rem 1rem;
  border: 1px solid lightgrey;
  white-space: nowrap;
}
.td--name {    
  overflow: hidden;
  text-overflow: ellipsis;
}
.row {
  position: relative;
  height: 2rem;
}



.btn {
  padding: 1rem .8rem;
  width: 7rem;
  background-color: white;
  margin-top: 2rem;
}
<div class="container">
    <div class="table-wrapper"></div>
    <div class="btn">Change Data</div>
</div>

**Edit:

@zer00ne's answer of using min-width did fix the row issue, but it's unfortunately caused other problems with text-overflow:ellipsis and column widths

If you click on the codepen, item 7 currently works and overflows as intended, and all the columns remain a fixed width, even if they aren't given a width in the css (extra space seems to be distributed evenly between them).

Adding min-width to the table, while fixing the row issue, unfortunately breaks this behaviour

Was hoping someone had any ideas on how I can keep the columns fixed (as the codepen currently behaves), while being able to add the pseudo element (or some way of achieving the same effect)

Thanks!

**Edit 2: I guess I could just manually divide up the total table width between each of the columns, but that seems a bit fragile

1 Answers

Update

I believe that the pseudo-element .active::after may still be the culprit. It feels like a code smell, a quick google lead me to nowhere official, but from what little I found coincides with what I was saying before, It's basically an extra cell in a single row. Because it's a pseudo-element, it's not part of the DOM so if you put one in a place where other elements cannot exist (like being a child of a <tr>), you may get unexpected results.

So here's what I did, it looks good. Try to break it and let me know if it actually does:

I changed where .active is generated -- it is now assigned to the first <td> of the first <tr> within the <tbody>. <td> can hold just about anything and it looks exactly like it did as it was on the <tr>.

Figure I

function formatData(data) {   // ↙️ Add the index parameter
  const rows = data.map((row, idx) => {
    return createHTMLRow(idx, row);
  });                   // ↖️ Pass it as the 1st parameter  
//...

Figure II

                     // ↙️ Pass the index reference as 1st parameter
function createHTMLRow(i, [id, name, age, location]) {
  const row = [
    `<td class="td--id ${i===0? 'active':''}">${id}</td>`,
//...                     ↖️ Here's the index reference interpolated

Original Solution

Still good for most cases. The update is for an edge case.

The table is at width: 100% which doesn't guarantee 100% with fixed tables that have undefined <th> widths. Change table width:

table {min-width: 100%}

// TABLE DATA
const headers = ['Id', 'Name', 'Age', 'Location'];

const datasetOne = [
  ['1', 'John Jones', '27', 'Swindon'],
  ['2', 'Pete Best', '23', 'Glasgow'],
  ['3', 'Jules West', '22', 'Exeter'],
  ['4', 'Kate Ford', '33', 'Fife'],
];
const datasetTwo = [
  ['5', 'Ruth Thompson', '27', 'Birmingham'],
  ['6', 'Dominic Lawson', '23', 'Greater London'],
  ['7', 'XXXXXXXXXX XXXXXXXXXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXX XXXXXXXXXXXXXX XXXXXXXx XXXXXX', '22', 'Manchester'],
  ['8', 'Nicholas Johnson', '33', 'Liverpool'],
];


const tableWrapper = document.querySelector('.table-wrapper');
const btn = document.querySelector('.btn');
let dataset = 1;

// Listeners
window.addEventListener('load', () => {
  const data = formatData(datasetOne);
  tableWrapper.insertAdjacentHTML('afterbegin', createTable(headers, data));
});
btn.addEventListener('click', () => {
  // Remove the table
  const table = document.querySelector('.table')
  table.parentElement.removeChild(table);

  // Create and insert a new table
  let data;
  if (dataset === 1) {
    data = formatData(datasetTwo);
    dataset = 2;
  } else if (dataset === 2) {
    data = formatData(datasetOne);
    dataset = 1;
  }
  tableWrapper.insertAdjacentHTML('afterbegin', createTable(headers, data));
});

// Functions to create the table
function formatData(data) {
  const rows = data.map((row, idx) => {
    return createHTMLRow(idx, row);
  });
  return rows;
}

function createHTMLRow(i, [id, name, age, location]) {
  const row = [
    `<td class="td--id ${i===0? 'active':''}">${id}</td>`,
    `<td class="td--name">${name}</td>`,
    `<td class="td--age">${age}</td>`,
    `<td class="td--location">${location}</td>`
  ];
  return row;
}

function createTable(theads, rows) {
  const markup = `
        <table class="table">
            <thead class="thead">
                <tr>
                    ${theads.map((thead) => {
                        return `<th class="th--${thead.toLowerCase()}">${thead}</th>`;
                    }).join('')} 
                </tr>
            </thead>
            <tbody class="tbody">
                ${
                    rows.map((row, index) => {
                        return `<tr class="row">${row.map(col => {
                            return `${col}`
                        }).join('')}</tr>`
                    }).join('')
                }
            </tbody>
        </table>
    `;
  return markup;
};
.active::after {
  position: absolute;
  content: '';
  left: 0;
  top: 0;
  width: 2px;
  height: 100%;  
  background-color: green;
}

* {
  margin: 0;
  box-sizing: border-box;
}

.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: firebrick;
}

.table-wrapper {
  display: flex;
  background-color: white;
  width: 30rem;
  overflow: hidden;
}

.table {
  display: table;
  table-layout: fixed;
  border-collapse: collapse;
  overflow: hidden;
  width: 100%;
}

th {
  text-align: start;
  padding: 1rem;
  background-color: lemonchiffon;
  border: 1px solid lightgrey;
}

.th--age,
.th--id {
  width: 4rem;
}

td {
  padding: .5rem 1rem;
  border: 1px solid lightgrey;
  white-space: nowrap;
}

.td--name {
  overflow: hidden;
  text-overflow: ellipsis;
}

.row {
  position: relative;
  height: 2rem;
}

.btn {
  padding: 1rem .8rem;
  width: 7rem;
  background-color: white;
  margin-top: 2rem;
}
<div class="container">
  <div class="table-wrapper"></div>
  <div class="btn">Change Data</div>
</div>

Related