1
\$\begingroup\$

A table that gets updated with new values as soon as they arrive. Each value is in its own row: A table that gets updated with new values as soon as they arrive. Each value is in its own row

An array of objects representing each a quote (ticker and price)

let array_of_quotes = [
  { ticker: "AAPL", price: 999 },
  { ticker: "AMZN", price: 957 },
  { ticker: "GOOG", price: 959 },
  { ticker: "YHOO", price: 225 },
  { ticker: "MSFT", price: 514 },
  { ticker: "AAPL", price: 218 },
  { ticker: "AMZN", price: 256 },
  { ticker: "AAPL", price: 526 },
  { ticker: "AMZN", price: 288 },
  { ticker: "MSFT", price: 514 },
  { ticker: "AMZN", price: 909 },
  { ticker: "YHOO", price: 225 },
];

A counter that keeps track of the current row to be updated

let counter = 0;

A function that updates the row, it gets the body by its id, then adds a row with the current count as its id. In said row, 4 table datas <td> are added with the ticker and current count as id so that, the function only has to get that <td> element and injects the price in its inner text leaving the other <td>s untouched

let table_s_body = document.getElementById("quote_table_body");
function table_row_updater(ticker_price) {
  table_s_body.innerHTML += `<tr id=row-${counter}></tr>`;
  let current_row = document.getElementById(`row-${counter}`);
  current_row.innerHTML += `<td id=AAPL-${counter}></td>`;
  current_row.innerHTML += `<td id=MSFT-${counter}></td>`;
  current_row.innerHTML += `<td id=AMZN-${counter}></td>`;
  current_row.innerHTML += `<td id=GOOG-${counter}></td>`;
  current_row.innerHTML += `<td id=YHOO-${counter}></td>`;
  let td_to_be_updated = document.getElementById(
    `${ticker_price.ticker}-${counter}`
  );
  td_to_be_updated.innerText = ticker_price.price;
  counter += 1;
}

of course, the counter gets incremented so that the next iteration starts a new.

For this demo purpose, i paced the "injection" of prices into their relative row's so it is less confusing.

array_of_quotes.forEach((quote, index) => {
  setTimeout(() => {
    table_row_updater(quote);
  }, (index + 1) * 1000);
});

What I would like to take out of your answers:

  • Is this solution clean? I feel it's a bit dirty to rely on a counter.
  • Couldn't the counter be achieved with a closure? If so, would anyone show me how?
  • How would you go about updating a single out of multiple ones in a table with an ever growing number of rows.

let array_of_quotes = [
  { ticker: "AAPL", price: 999 },
  { ticker: "AMZN", price: 957 },
  { ticker: "GOOG", price: 959 },
  { ticker: "YHOO", price: 225 },
  { ticker: "MSFT", price: 514 },
  { ticker: "AAPL", price: 218 },
  { ticker: "AMZN", price: 256 },
  { ticker: "AAPL", price: 526 },
  { ticker: "AMZN", price: 288 },
  { ticker: "MSFT", price: 514 },
  { ticker: "AMZN", price: 909 },
  { ticker: "YHOO", price: 225 },
];

let counter = 0;
let table_s_body = document.getElementById("quote_table_body");
function table_row_updater(ticker_price) {
  table_s_body.innerHTML += `<tr id=row-${counter}></tr>`;
  let current_row = document.getElementById(`row-${counter}`);
  current_row.innerHTML += `<td id=AAPL-${counter}></td>`;
  current_row.innerHTML += `<td id=MSFT-${counter}></td>`;
  current_row.innerHTML += `<td id=AMZN-${counter}></td>`;
  current_row.innerHTML += `<td id=GOOG-${counter}></td>`;
  current_row.innerHTML += `<td id=YHOO-${counter}></td>`;
  let td_to_be_updated = document.getElementById(
    `${ticker_price.ticker}-${counter}`
  );
  td_to_be_updated.innerText = ticker_price.price;
  counter += 1;
}

array_of_quotes.forEach((quote, index) => {
  setTimeout(() => {
    table_row_updater(quote);
  }, (index + 1) * 1000);
});
<table>
  <thead>
    <tr>
      <th>AAPL</th>
      <th>MSFT</th>
      <th>AMZN</th>
      <th>GOOG</th>
      <th>YHOO</th>
    </tr>
  </thead>
    <tbody id="quote_table_body"></tbody>
</table>

\$\endgroup\$
3
  • \$\begingroup\$ can you please post a working example of your code in the form of a [repro]? That would make a refactorization of it much cleaner and easier. \$\endgroup\$
    – tacoshy
    Commented Dec 12, 2023 at 15:16
  • \$\begingroup\$ I'm afraid I don't know what a [repro] stands for. Could you provide me with a link to the approriate help center section? Thank you \$\endgroup\$
    – dhiaagr
    Commented Dec 12, 2023 at 16:11
  • 1
    \$\begingroup\$ @tacoshy I believe I understood what you meant, a reproducible code snippet. I included one. Also, thank you for your suggestion as it made me realize I failed to include one crucial line of code, the one that grabs the body by its id. Let me know how I can improve my post further. Thank you \$\endgroup\$
    – dhiaagr
    Commented Dec 12, 2023 at 16:20

3 Answers 3

3
\$\begingroup\$

Is this solution clean? I feel it's a bit dirty to rely on a counter.
Couldn't the counter be achieved with a closure? If so, would anyone show me how?

You don't need a counter at all. If you want to use a counter then you don't need an extra variable and iterate it by yourself.

First, if you iterate a counter then don't do it your way counter += 1 do it the easy way counter++.

However, as said, you don't need a counter and if you want one you could simply use your index from your forEach iteration array_of_quotes.forEach((quote, index) => { ... }), which is exactly this by default.

The first issue I have with your code is the usage of element.innerHTML += {HTML Code} which is an unclean method that should never be done. Unfortunately, it is still taught in schools, tutorials, and so on. Despite the fact that it is unclean, it is also comparably "slow" and can pose security issues such as XSS injections.
The "only really correct way" to add elements to the DOM is by using the createElement method. Create an element and then append it to the DOM!

The next issue I have is the missing abstraction from your table. Your JS code always assumes and relies on, that your table has only 5 columns with that exact name. If a column name changes or another column is added your JS would stop working correctly. An update would have to be done both in the JS and the HTML code which adds maintenance load and effort as well as adding error possibilities.

For that, I wrote you a little function that reads out how many columns you have and how they are named. They push those names to an array and with that I can keep the JS on its own up to date with the HTML table:

function getColumns() {
  let columns_array = [];
  THEAD_COLS.forEach(col => {
    columns_array.push(col.textContent);
  })
  return columns_array;
}

With that array, I can simply iterate through that array while creating a new road and read cells for every existing column and do not have to hard code every single cell:

COLUMNS.forEach(cell => {
  const CELL = document.createElement('td');
  ROW.appendChild(CELL);
})

The last real issue I have is to add an ID to every single cell. Rather then doing that use the tool that you have for such purpose: data-attribtues!

How would you go about updating a single out of multiple ones in a table with an ever growing number of rows?

First to add more data later on that is not covered by the array, you can write a simple function to read out the last used index and then execute your existing function while sending a new dataset and the last used index + 1;

If you want to update an existing row, you need to know the index of the row that needs to be updated. Then you can call that row by searching for the data-row attribute. Eg. the first row you get by using document.querySelector('tr[data-row="0"]')

let array_of_quotes = [
  { ticker: "AAPL", price: 999 },
  { ticker: "AMZN", price: 957 },
  { ticker: "GOOG", price: 959 },
  { ticker: "YHOO", price: 225 },
  { ticker: "MSFT", price: 514 },
  { ticker: "AAPL", price: 218 },
  { ticker: "AMZN", price: 256 },
  { ticker: "AAPL", price: 526 },
  { ticker: "AMZN", price: 288 },
  { ticker: "MSFT", price: 514 },
  { ticker: "AMZN", price: 909 },
  { ticker: "YHOO", price: 225 },
];

const TBODY = document.getElementById("quote_table_body");
const THEAD_COLS = document.querySelectorAll('th');
const COLUMNS = getColumns();

window.addEventListener('DOMContentLoaded', function() {
  array_of_quotes.forEach(function(quote, index) {
    setTimeout(() => {
      table_row_updater(quote, index);
    }, (index + 1) * 1000);
  });
});

function table_row_updater(quote, index) {
  const TICKER = quote.ticker;
  const PRICE = quote.price;
  
  const ROW = document.createElement('tr');
  ROW.dataset.row = index;
  
  COLUMNS.forEach(cell => {
    const CELL = document.createElement('td');
    CELL.dataset.col = cell;
    CELL.textContent = (cell === TICKER) ? PRICE : '';
    ROW.appendChild(CELL);
  })
  
  TBODY.appendChild(ROW);
}

function getColumns() {
  let columns_array = [];
  THEAD_COLS.forEach(col => {
    columns_array.push(col.textContent);
  })
  return columns_array;
}


/* function to add an aditional row not covered by the array */
function addTableRow(newData) {
  let nextIndex = parseInt(TBODY.lastChild.dataset.row) + 1;
  table_row_updater(newData, nextIndex)
}

/* delay and random data as proof of concept that the above function works */
setTimeout(function() {
  const newData = { ticker: "AAPL", price: 747 };
  addTableRow(newData); 
}, 15000);  
<table>
  <thead>
    <tr>
      <th>AAPL</th>
      <th>MSFT</th>
      <th>AMZN</th>
      <th>GOOG</th>
      <th>YHOO</th>
    </tr>
  </thead>
  <tbody id="quote_table_body"></tbody>
</table>

If you have more questions about it or why some things where changed or parts you do not understand, feel free to add a comment.

\$\endgroup\$
1
  • \$\begingroup\$ Clear and concise code + rich explanation. Thank you ( : \$\endgroup\$
    – dhiaagr
    Commented Dec 13, 2023 at 6:24
1
\$\begingroup\$

Couldn't the counter be achieved with a closure? If so, would anyone show me how?

A solution calling insertRow and insertCell methods, of table respectively of TableRow, and additionally comparing the ticker with the inner text of the table header cells would leverage avoiding indexes and counters. Mimic listeners behaviour building an array of arrow functions base on the table header cells and for each quote insert a row in the table and call the arrow functions passing the row and the quote...

var expounder = (function() {

    return {
        of: (table) => {
            
            var listeners = [].slice.call(table.rows[0].cells)
                              .map( (cell) => { 
                                  return (row, quote) => {
                                      var td = row.insertCell(row.cells.length);
                                      ( cell.innerText.trim().toLowerCase() == quote.ticker.toLowerCase() ) 
                                      ? td.innerText = quote.price 
                                      : td.innerText = " ";
                              }; } );
            
            return {
                expound: (expoundee) => {
                    expoundee.forEach( (quote) => {
                        var row = table.insertRow();
                        listeners.forEach( (listener) => { listener(row, quote); } );
                    } );
                }    
            };
        }
    };
})();

A plain usage example would be...

<html>
<head>
<script type="text/javascript">
var expounder = (function() {

    return {
        of: (table) => {
            
            var listeners = [].slice.call(table.rows[0].cells)
                              .map( (cell) => { 
                                  return (row, quote) => {
                                      var td = row.insertCell(row.cells.length);
                                      ( cell.innerText.trim().toLowerCase() == quote.ticker.toLowerCase() ) 
                                      ? td.innerText = quote.price 
                                      : td.innerText = " ";
                              }; } );
            
            return {
                expound: (expoundee) => {
                    expoundee.forEach( (quote) => {
                        var row = table.insertRow();
                        listeners.forEach( (listener) => { listener(row, quote); } );
                    } );
                }    
            };
        }
    };
})();

var waver = (function() {
    return { between: function(min, max) {
                 return Math.floor( Math.random() * ( max - min + 1 ) + min );
           }
           , until: function(max) {
                 return Math.floor( Math.random() * ( max + 1 ) );
           }
};})();

var scheduleExpound = (table) => {
    var quotes = [ [ { ticker: "1", price: 999 }
                   , { ticker: "2", price: 957 } ]
                 , [ { ticker: "5", price: 959 } 
                   , { ticker: "3", price: 225 } ]
                 , [ { ticker: "1", price: 514 } 
                   , { ticker: "4", price: 218 }
                   , { ticker: "2", price: 256 } ]
                 ];
    
    var deployer = expounder.of(table);
    quotes.forEach( (quote) => {
        setTimeout( () => { deployer.expound(quote); }, waver.between(1000, 5000) );
    } );
};
</script>
</head>
<body>
<table id="demo-table">
  <thead>
      <tr>
      <td>1</td>
      <td>2</td>
      <td>3</td>
      <td>4</td>
      <td>5</td>
      </tr>
  </thead>
</table>
<script type="text/javascript">scheduleExpound(document.getElementById("demo-table"));</script>
</body>
</html>
\$\endgroup\$
1
  • 1
    \$\begingroup\$ @tacoshy "no I cannot as this is against the group rules. But as a mod you surely get access without invitation and/or read the logs." I do not know the group you are referencing. Please can you provide a link to the group or tell me the name of a user in the group so I can access the information you are talking about. \$\endgroup\$
    – Peilonrayz
    Commented Dec 15, 2023 at 10:28
1
\$\begingroup\$

Couldn't the counter be achieved with a closure? If so, would anyone show me how?

Following is a walk through the solution from a previous answer, specifically this solution:

var expounder = (function() {

    return {
        of: (table) => {

            var listeners = [].slice.call(table.rows[0].cells)
                              .map( (cell) => {
                                  return (row, quote) => {
                                      var td = row.insertCell(row.cells.length);
                                      ( cell.innerText.trim().toLowerCase() == quote.ticker.toLowerCase() ) 
                                      ? td.innerText = quote.price 
                                      : td.innerText = " ";
                              }; } );

            return {
                expound: (expoundee) => {
                    expoundee.forEach( (quote) => {
                        var row = table.insertRow();
                        listeners.forEach( (listener) => { listener(row, quote); } );
                    } );
                }
            };
        }
    };
})();

var scheduleExpound = (table) => {
    var quotes = [ [ { ticker: "1", price: 999 }
                   , { ticker: "2", price: 957 } ]
                 , [ { ticker: "5", price: 959 }
                   , { ticker: "3", price: 225 } ]
                 , [ { ticker: "1", price: 514 }
                   , { ticker: "4", price: 218 }
                   , { ticker: "2", price: 256 } ]
                 ];

    var deployer = expounder.of(table);
    quotes.forEach( (quote) => {
        setTimeout( () => { deployer.expound(quote); }, waver.between(1000, 5000) );
    } );
};

An empty object defined using object literal form: {}. It is equivalent to writing new Object()

An object with one method called of defined using object literal form: { of: () => { . . . } }. It is equivalent to writing...

function FactoryObject {}

FactoryObject.prototype.of = function() { . . . }

var factory = new FactoryObject();

...where FactoryObject is an arbitrary name to exemplify an equivalent.

An empty auto callable function: (function() {} )();

An auto callable function returning an object with one method called of...

var expounder = (function() { return { of: () => { . . . } }; } )();

An auto callable function returning an object with one method called of receiving one argument called table returning an object with one method called expound receiving one argument called expoundee...

var expounder = (function() { 
                     return { of: (table) => { 
                                  return { expound: (expoundee) => { . . . } };
                              }
                     };
                })();

With the difference of scope, that in case of auto callable function it is bounded to the wrapping function, it is equivalent to writing...

var expounder = { on: (table) => { return { expound: (expoundee) => { . . . } }; } };

table.rows[0].cells is an HTMLCollection converted to an array by calling slice method with a single argument...

[].slice.call(table.rows[0].cells)

It would be equivalent to...

table.rows[0].cells.slice(0, table.rows[0].cells.length)

...if HTMLCollection would have been an Array.

Build an array of closures by mapping each cell to a closure, that is an arrow function with the scope bounded to the mapping function this way having access to the mapped cell, with two parameters row and quote (row, quote) => { . . . } that are passed at call time (e.g. in expound method)...

var listeners = [].slice.call(table.rows[0].cells)
                  .map( (cell) => {
                            return (row, quote) => {
                                var td = row.insertCell(row.cells.length);
                                ( cell.innerText.trim().toLowerCase() == quote.ticker.toLowerCase() ) 
                                ? td.innerText = quote.price 
                                : td.innerText = " ";
                            };
                        }
                   );

An object, returned by the of method of expounder object, with one method called expound receiving one argument called expoundee that notifies quote event listeners passing them the quote and row to display...

{ expound: (expoundee) => {
               expoundee.forEach( (quote) => {
                   var row = table.insertRow();
                   listeners.forEach( (listener) => { listener(row, quote); } );
               } );
           }
}

An arrow function to schedule quotes displaying...

var scheduleExpound = (table) => { 
    var quotes = [ [ { ticker: "1", price: 999 }
                   , { ticker: "2", price: 957 } ]
                 , [ { ticker: "5", price: 959 }
                   , { ticker: "3", price: 225 } ]
                 , [ { ticker: "1", price: 514 }
                   , { ticker: "4", price: 218 }
                   , { ticker: "2", price: 256 } ]
                 ];

    var deployer = expounder.of(table);
    quotes.forEach( (quote) => {
        setTimeout( () => { deployer.expound(quote); }, waver.between(1000, 5000) );
    } );
};

...it is a shallow implementation of Observer design pattern.

An array of arrays simulating quote events...

var quotes = [ [ { ticker: "1", price: 999 }
                 , { ticker: "2", price: 957 } ]
             , [ { ticker: "5", price: 959 }
                 , { ticker: "3", price: 225 } ]
             , [ { ticker: "1", price: 514 }
                 , { ticker: "4", price: 218 }
                 , { ticker: "2", price: 256 } ]
             ];

Shallow implementation to build and register quote event listener...

var deployer = expounder.of(table);

Shallow implementation to source quote events...

quotes.forEach( (quote) => {
    setTimeout( () => { deployer.expound(quote); }, waver.between(1000, 5000) );
} );

...it is equivalent to dispatchEvent method if quote event would have implement Event interface.

\$\endgroup\$
1
  • \$\begingroup\$ what the hell is going on in this thread? \$\endgroup\$
    – radarbob
    Commented Dec 16, 2023 at 20:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.