Use the DOM APIs
For performance avoid adding markup (HTML) to the page via JavaScript. The DOM APIs are much faster and can be abstracted to make more readable code (DOM APIs are very verbose).
Source complexity
Your solution is convoluted and hard to read. It looks like you tackled the whole problem in one big step. More than a dozen lines of code should be seen as more than one problem to solve.
To solve complex or multi step problem break them into smaller single role parts.
Find all rows names
Find all columns names
Find value by row column name
Create DOM elements
Append a DOM elements
Create a table.
Each of these sub problems can then be solved by defining a function. When you have all the functions you can solve the main problem by combining the sub problems.
Try to make the functions generic. Property names should be dynamic so that you need only minor changes when the data changes. The example show 4 tables, to change your code to provide the different tables would take a lot more work than adding 3 more calls to the main function.
Example
The rewrite breaks the problem into the pars described above. The title, and the names of the row and column properties are passed to the function.
You can see the flexibility of this approach as the table can easily be rotated and transformed by changing the argument order and properties to use for columns and rows.
const notes = [{note: "n1", subject: "subject1", value: 10 }, {note: "n2", subject: "subject2", value: 15 }, {note: "n2", subject: "subject2", value: 5 }, {note: "n3", subject: "subject2", value: 20 }];
const tag = (tagName, props = {}) => Object.assign(document.createElement(tagName), props);
const txtTag = (tagName, str, props = {}) => tag(tagName, {textContent: str, ...props});
const append = (el, ...sibs) => sibs.reduce((p, sib) => (p.appendChild(sib), p), el);
append(document.body,
createTable("Subjects", "note", "subject", "value", notes),
createTable("Notes", "subject", "note", "value", notes),
createTable("Notes", "value", "note", "value", notes),
createTable("Values", "subject", "value", "value", notes));
function createTable(title, colKey, rowKey, valKey, data) {
const byKey = key => [...new Set(data.map(rec => rec[key]))];
const byRowCol = (row, col) => data.reduce((val, rec) =>
val + (rec[rowKey] === row && rec[colKey] === col ? rec[valKey] : 0), 0);
const rows = byKey(rowKey), cols = byKey(colKey);
return append(tag("table"),
append(tag("tbody"),
append(tag("tr"),
txtTag("th", title),
...cols.map(str => txtTag("th", str))
),
...rows.map(row =>
append(tag("tr"),
txtTag("th", row),
...cols.map(col => txtTag("td", byRowCol(row, col)))
)
)
)
)
}
* {font-family: arial}
table, tr, th, td {
border-collapse: collapse;
border: 1px solid;
padding: 3px 10px;
margin: 4px;
}
Answer from Blindman67 on Stack ExchangeUse the DOM APIs
For performance avoid adding markup (HTML) to the page via JavaScript. The DOM APIs are much faster and can be abstracted to make more readable code (DOM APIs are very verbose).
Source complexity
Your solution is convoluted and hard to read. It looks like you tackled the whole problem in one big step. More than a dozen lines of code should be seen as more than one problem to solve.
To solve complex or multi step problem break them into smaller single role parts.
Find all rows names
Find all columns names
Find value by row column name
Create DOM elements
Append a DOM elements
Create a table.
Each of these sub problems can then be solved by defining a function. When you have all the functions you can solve the main problem by combining the sub problems.
Try to make the functions generic. Property names should be dynamic so that you need only minor changes when the data changes. The example show 4 tables, to change your code to provide the different tables would take a lot more work than adding 3 more calls to the main function.
Example
The rewrite breaks the problem into the pars described above. The title, and the names of the row and column properties are passed to the function.
You can see the flexibility of this approach as the table can easily be rotated and transformed by changing the argument order and properties to use for columns and rows.
const notes = [{note: "n1", subject: "subject1", value: 10 }, {note: "n2", subject: "subject2", value: 15 }, {note: "n2", subject: "subject2", value: 5 }, {note: "n3", subject: "subject2", value: 20 }];
const tag = (tagName, props = {}) => Object.assign(document.createElement(tagName), props);
const txtTag = (tagName, str, props = {}) => tag(tagName, {textContent: str, ...props});
const append = (el, ...sibs) => sibs.reduce((p, sib) => (p.appendChild(sib), p), el);
append(document.body,
createTable("Subjects", "note", "subject", "value", notes),
createTable("Notes", "subject", "note", "value", notes),
createTable("Notes", "value", "note", "value", notes),
createTable("Values", "subject", "value", "value", notes));
function createTable(title, colKey, rowKey, valKey, data) {
const byKey = key => [...new Set(data.map(rec => rec[key]))];
const byRowCol = (row, col) => data.reduce((val, rec) =>
val + (rec[rowKey] === row && rec[colKey] === col ? rec[valKey] : 0), 0);
const rows = byKey(rowKey), cols = byKey(colKey);
return append(tag("table"),
append(tag("tbody"),
append(tag("tr"),
txtTag("th", title),
...cols.map(str => txtTag("th", str))
),
...rows.map(row =>
append(tag("tr"),
txtTag("th", row),
...cols.map(col => txtTag("td", byRowCol(row, col)))
)
)
)
)
}
* {font-family: arial}
table, tr, th, td {
border-collapse: collapse;
border: 1px solid;
padding: 3px 10px;
margin: 4px;
}
A short review;
- JS should be lowerCamelCase, so
make_matrix->makeMatrix array_objsshould probably be just beobjects- I am not a big fan of HTML in JS, consider using a template
- In
makeMatrixyou both convert the data into a new structure and build the output, I would split this across 2 functions - I see a tendency to name variables more after what they are than what they contain, I hope my counter-examples shows what I mean
- I find your code very dense and hard to read
- However I really like the trick to create subjects and notes, will steal that ;)
let notes = [
{ note: "n1", subject: "subject1", value: 10 },
{ note: "n2", subject: "subject2", value: 15 },
{ note: "n2", subject: "subject2", value: 5 },
{ note: "n3", subject: "subject2", value: 20 },
];
function restructureSubjectScores(subjectScores){
const out = {};
for(const score of subjectScores){
out[score.subject] = out[score.subject] || {};
out[score.subject][score.note] = score.value;
}
return out;
}
function buildTable(noteScores){
const subjects = [...new Set(noteScores.map(({ subject }) => subject))];
const notes = [...new Set(noteScores.map(({ note }) => note))];
const tableData = restructureSubjectScores(noteScores);
const table = document.getElementById('tableTemplate').content.firstElementChild.cloneNode(true);
const header = table.querySelector('thead tr');
header.innerHTML += notes.map(column => `<th>${column}</th>`).join("");
const tbody = table.querySelector("tbody");
const rowTemplate = document.getElementById('rowTemplate').content.firstElementChild;
for(const subject of subjects){
const row = rowTemplate.cloneNode(true);
row.querySelector("th").textContent = subject;
for(const note of notes){
row.innerHTML += `<td>${tableData[subject][note] || 0}</td>`;
}
tbody.appendChild(row);
}
return table;
}
document.querySelector(".content").appendChild(buildTable(notes));
table {
border-collapse: collapse;
border: 1px solid;
}
tr,
th,
td {
border: 1px solid;
padding: 3px 10px;
}
<div class="content"></div>
<template id="tableTemplate">
<table>
<thead><tr>
<th class="subject">Subjects</th>
</tr></thead>
<tbody>
</tbody>
</table>
</template>
<template id="rowTemplate">
<tr>
<th class="subject"></th>
</tr>
</template>
You can loop through the array and for each element add a row to a newly forged table that will be added to the document at the end.
This is a demo:
Copylet players = [
{name: 'Player1',score:10},
{name: 'Player2',score: 7},
{name: 'Player3',score:3}
];
const newTable = document.createElement("table");
newTable.innerHTML = "<thead><th>Player</th><th>Score</th></thead>";
for(player of players){
const newRow = document.createElement("tr");
const tdPlayer = document.createElement("td");
const tdScore = document.createElement("td");
tdPlayer.textContent = player.name;
tdScore.textContent = player.score;
newRow.appendChild(tdPlayer);
newRow.appendChild(tdScore);
newTable.appendChild(newRow);
}
const target = document.getElementById('target');
target.appendChild(newTable);
Copytable{
border: solid 1px black;
}
table td{
border: solid 1px black;
}
Copy<div id="target">
</div>
Run code snippetEdit code snippet Hide Results Copy to answer Expand
You can use something like
Copy<body>
<div class="main-container">
<table>
<thead>
<tr>
<th>player</th>
<th>score</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<script>
const data = [{ name: 'Player 1', score: 10 },
{ name: 'Player 2', score: 7 },
{ name: 'Player 3', score: 3 }]
const table = document.querySelector('tbody')
data.forEach((item) => {
table.innerHTML = table.innerHTML + `<tr>
<td>${item.name}</td>
<td>${item.score}</td>
</tr>`
})
</script>
Two issues at first:
- Is that Name or Nome? Please fix your typo (I know Nome is French, so if it's not typo, I suggest you introduce an i18n solution).
- Also please mind your syntax errors (some have given suggested edits that you could consider).
After that, here's my approach for programmatically create tables based on input data:
function buildTable(labels, objects, container) {
var table = document.createElement('table');
var thead = document.createElement('thead');
var tbody = document.createElement('tbody');
var theadTr = document.createElement('tr');
for (var i = 0; i < labels.length; i++) {
var theadTh = document.createElement('th');
theadTh.innerHTML = labels[i];
theadTr.appendChild(theadTh);
}
thead.appendChild(theadTr);
table.appendChild(thead);
for (j = 0; j < objects.length; j++) {
var tbodyTr = document.createElement('tr');
for (k = 0; k < labels.length; k++) {
var tbodyTd = document.createElement('td');
tbodyTd.innerHTML = objects[j][labels[k].toLowerCase()];
tbodyTr.appendChild(tbodyTd);
}
tbody.appendChild(tbodyTr);
}
table.appendChild(tbody);
container.appendChild(table);
}
var labels1 = ['ID', 'Name'];
var objects1 = [
{"id": "1", 'name': "richard"},
{"id": "2", 'name': "santos"}
];
var labels2 = ['ID', 'NOME'];
var objects2 = [
{"id": "1", 'nome': "richard"},
{"id": "2", 'nome': "adriana"}
];
var labels3 = ['ID', 'NAME', 'PLATE'];
var objects3 = [
{"id": "1", 'name': "jetta", 'plate': "DFG-1222"},
{"id": "2", 'name': "fusion", 'plate': "DFF-3342"}
];
buildTable(labels1, objects1, document.getElementById('a'));
buildTable(labels2, objects2, document.getElementById('b'));
buildTable(labels3, objects3, document.getElementById('c'));
table {
border-collapse: collapse;
}
th, td {
border: 1px solid black;
}
<div id="a"><p>Table 1</p></div>
<div id="b"><p>Table 2</p></div>
<div id="c"><p>Table 3</p></div>
This is a working procedure from our project. It has three parameters:
htmlTable(selector, data_array [, column_names]);
column_names parameter is optional: if you omit it the function creates column names from first row (if it exists).
It creates HTML tags directly into the DOM, but it can be rewritten to generate HTML as a string if you need . See the working snippet below:
var labels = ['id','name'];
var object = [{"id":"1",'name': "richard"},{"id":"2",'name': "santos"}];
htmlTable("#res0",object, labels);
var labels = ['id','nome'];
var object = [{"id":"1",'nome': "richard"},{"id":"2",'nome': "adriana"}];
htmlTable("#res1",object, labels);
var labels = ['id','name', 'plate'];
var object = [{"id":"1",'name': "jetta",'plate': "DFG-1222"},
{"id":"2",'name': "fusion",'plate': "DFF-3342"}];
htmlTable("#res2",object, labels);
// Without labels array
var data3 = [{a:1,c:2},{a:3,c:3}];
htmlTable("#res3",data3);
function htmlTable(selector, data, columns) {
var sel = document.querySelector(selector);
if(!sel) {
throw new Error('Selected HTML element is not found');
};
if((!columns) || columns.length == 0) {
columns = Object.keys(data[0]);
}
var tbe = document.createElement('table');
var thead = document.createElement('thead');
tbe.appendChild(thead);
var tre = document.createElement('tr');
for(var i=0;i<columns.length;i++){
var the = document.createElement('th');
the.textContent = columns[i];
tre.appendChild(the);
}
thead.appendChild(tre);
var tbody = document.createElement('tbody');
tbe.appendChild(tbody);
for(var j=0;j<data.length;j++){
var tre = document.createElement('tr');
for(var i=0;i<columns.length;i++){
var the = document.createElement('td');
the.textContent = data[j][columns[i]];
tre.appendChild(the);
}
tbody.appendChild(tre);
};
emptyDOMChildren(sel);
sel.appendChild(tbe);
};
// Utility function to fast delete all children of element if it is not empty
// Can be replaced with simple but relatively "slower" container.innerHTML = "";
function emptyDOMChildren (container){
var len = container.childNodes.length;
while (len--) {
container.removeChild(container.lastChild);
};
};
<div id="res0"></div>
<div id="res1"></div>
<div id="res2"></div>
<div id="res3"></div>
Explanation
What you want is to fill a table (or another DOMElement) in HTML, with your JavaScript, which is executed dynamically once the page is loaded and your JSON object is received.
You want to loop through the object. The best way to do so would be with a for loop, and making sure our looping variable remains valid for the length of our object (all its attributes).
The best way to get the length of a JSON object is through myJSONObject.length: You select the keys of myJSONObject and return their count.
You can access the values stored in your JSON Object the following way, in your for loop (assuming the looping variable defined is named i): myJSONObject[i].theAttributeIWantToGet
Price formatting breakdown
Now, those prices need to have a proper format, don't they? So we'll check if any of the value attribute has less than 2 characters after the . within them. If they do, we add another decimal 0. We also add a $ before writing the formatted value. Here is a breakdown of how it works:
obj[i].value.toString().substring(startIndex, length)- We want to check the length after the
.sign, so our startIndex will be the position of this dot within our string. obj[i].value.toString().substring(obj[i].value.toString().indexOf('.'),length)- We now need to set the length. We want to find the length of all what's after the dot, so we'll take the length of the whole string just to be safe.
Final result:
obj[i].value.toString().substring(obj[i].value.toString().indexOf('.'), obj[i].value.toString().length) < 2- This will return true or false. If it's true: There's less than 2 digits after the dot !
We add the
ifstatement and the last zero:if (obj[i].value.toString().substring(obj[i].value.toString().indexOf('.'), obj[i].value.toString().length) < 2) obj[i].value += "0";
- We want to check the length after the
Also: Why I use innerHTML instead of appendChild().
Solution
JSFiddle
HTML
<table>
<tbody id="tbody"></tbody>
</table>
JSON
[{
"key": "apple",
"value": 1.90
}, {
"key": "berry",
"value": 1.7
}, {
"key": "banana",
"value": 1.5
}, {
"key": "cherry",
"value": 1.2
}]
JavaScript
Note: The JSON object will be named obj in this instance.
var tbody = document.getElementById('tbody');
for (var i = 0; i < obj.length; i++) {
var tr = "<tr>";
/* Verification to add the last decimal 0 */
if (obj[i].value.toString().substring(obj[i].value.toString().indexOf('.'), obj[i].value.toString().length) < 2)
obj[i].value += "0";
/* Must not forget the $ sign */
tr += "<td>" + obj[i].key + "</td>" + "<td>$" + obj[i].value.toString() + "</td></tr>";
/* We add the table row to the table body */
tbody.innerHTML += tr;
}
JSFiddle
It can be simply done by a small & smart process:
<table cellpadding="2" cellspacing="2" border="0" bgcolor="#dfdfdf" width="40%" align="center">
<thead>
<tr>
<th>Name</th>
<th width="20%">Age</th>
<th width="12%">Status</th>
</tr>
</thead>
<tbody id="tableData"></tbody>
</table>
<script type="text/javascript">
var mainObj = [
{
name: "Kapil",
age: 21,
status: "Active"
},
{
name: "John",
age: 28,
status: "Inactive"
},
{
name: "Deos",
age: 18,
status: "Active"
}
];
var k = '<tbody>'
for(i = 0;i < mainObj.length; i++){
k+= '<tr>';
k+= '<td>' + mainObj[i].name + '</td>';
k+= '<td>' + mainObj[i].age + '</td>';
k+= '<td>' + mainObj[i].status + '</td>';
k+= '</tr>';
}
k+='</tbody>';
document.getElementById('tableData').innerHTML = k;
</script>
Here's a function that will use the dom instead of string concatenation.
function createTable(tableData) {
var table = document.createElement('table');
var tableBody = document.createElement('tbody');
tableData.forEach(function(rowData) {
var row = document.createElement('tr');
rowData.forEach(function(cellData) {
var cell = document.createElement('td');
cell.appendChild(document.createTextNode(cellData));
row.appendChild(cell);
});
tableBody.appendChild(row);
});
table.appendChild(tableBody);
document.body.appendChild(table);
}
createTable([["row 1, cell 1", "row 1, cell 2"], ["row 2, cell 1", "row 2, cell 2"]]);
This is pretty easy to do with a double for loop.
function makeTableHTML(myArray) {
var result = "<table border=1>";
for(var i=0; i<myArray.length; i++) {
result += "<tr>";
for(var j=0; j<myArray[i].length; j++){
result += "<td>"+myArray[i][j]+"</td>";
}
result += "</tr>";
}
result += "</table>";
return result;
}