I have managed to animate the height of the result tables for totals calculator. Previously I would calculate the height at back end and would send the value to JavaScript’s animate function every time a data was loaded in to tables. However this approach had a downside, as it would not calculate the height of table (or div elements) accurately.

So I have changed the approach of how the table animations are done: instead of trying to calculate the table height by the amount of rows in the table at back end, I now create an identical table and fill it with the same data as the main one. The way this works is like this:
- A function is called that creates HTML elements, the important ones for animation are these: container, that’s height is used for animation, table to store data and canvas to render a chart. It also creates an identical HTML elements that are used for containers height calculation.
- Data gets passed to the function that fills both tables (the main one and the copy one).
- At the start of this function a height value is picked of the main table.
- Every time a row is added to the main table, it’s containers height is being reset to the start height (this way the table height is kept the same).
- The copy table (I called it “limbo”) has all the rows added to it without any changes to it’s original height.
- Once all rows are added to both tables, the height value of “limbo” table is picked and used to animate main table to it’s new height.
And this approach worked without any issues. The only issue is that it practically doubled the code in JavaScript as now I have to create twice as many tables and charts, but it’s a small price to pay for a solution that works 😉
HTML and CSS
In HTML I created a section where I store the limbo div’s, that is moved out of sight with CSS so users are not aware of it. For demonstration I will only use earnings table and chart.
<!--totals calculator tables and charts-->
<!--In actual html file this is nested in several div elements but I included it here just for demonstration purposes-->
<div class="col-xs-12 P0" id="earningsTotalsTableSection"></div>
<section class="container-fluid moveLimboOutOfSight P0">
<div class="row P0">
<div class="col-md-3 hidden-xs"></div>
<div class="col-md-6 col-xs-12">
<div class="col-xs-12 P0" id="earningsTotalsTableSectionLimbo"></div>
</div>
</div>
</section>
To move the section with “limbo” div’s I created a class called moveLimboOutOfSight. Also changed some border rules compared to the previously posted CSS code to accommodate changes done to HTML layout.
.BT {
border-top: 1px solid #cccccc; }
div div .table {
border-collapse: collapse;
margin: 0px; }
.table-responsive {
margin: 0px;
margin-top: 20px;
border: 1px solid #cccccc;
-webkit-box-shadow: 0 8px 6px -6px #4d4d33;
box-shadow: 0 8px 6px -6px #4d4d33;
padding: 0px;
overflow: hidden; }
div div table caption {
position: relative;
height: 40px;
padding: 0px 14px;
border: none;
border-bottom: 1px solid #cccccc; }
table caption i, .chartTitle i, .fa-info-circle {
position: absolute;
right: 4px;
top: 2px;
cursor: pointer;
color: #3bb300;
font-size: 35px; }
table caption i:hover {
color: green; }
table caption i:focus {
color: green; }
table caption i:active {
color: green; }
.table thead {
background-color: #f2f2f2;
color: #4d4d33;
height: 35px; }
.table thead tr th:first-child, .table thead tr th:nth-child(2) {
font-weight: 400;
font-size: 20px;
border-bottom: 1px solid #cccccc; }
.table thead tr th:first-child {
border-right: 1px solid #cccccc;
text-align: left;
padding: 0px 0px 0px 15px; }
.table thead tr th:nth-child(2) {
padding: 0px 15px 0px 0px;
text-align: right; }
.table tbody tr td {
font-size: 18px;
padding: 0px;
border: none; }
.table tbody tr td:first-child {
text-align: left;
padding-left: 5px;
border-right: 1px solid #cccccc !important; }
.table tbody tr td:nth-child(2) {
padding-right: 5px;
text-align: right; }
.table tfoot {
border-top: 1px solid #cccccc; }
canvas {
position: relative; }
//class to move the limbo tables out of user's sight
.moveLimboOutOfSight {
position: fixed;
left: -10000px;
width: 100%; }
JavaScript
Now lets explain JavaScript. The createTotalsElements() function now creates two identical tables: one is shown to a user with data, another one, using CSS is moved out of user’s sight, and only used to calculate height. For demonstrative purposes I will only show code for payments table.
const createTotalsElements = ()=>{
//creating a payments table
let earningsTableTotalsElements = {
title:"Total Earnings For Period",
infoIconID:false,
appendThead:true,
tbodyID:"totalEarningsTable",
renderChart: true,
tfootID: "totalEarningsChartDiv",
canvasID: "totalEarningsChart",
symbol:" £",
animationDivID:"totalEarningsTableAnimation",
eyeIconTitle: false,
eyeIconChart: false
};
createTableAndChartSection(document.getElementById("earningsTotalsTableSection"), earningsTableTotalsElements);
//creating an empty table for height calculation
let earningsTableTotalsElementsLimbo = {
title:"",
infoIconID:false,
appendThead:true,
tbodyID:"totalEarningsTableLimbo",
renderChart: true,
tfootID: "totalEarningsChartDivLimbo",
canvasID: "totalEarningsChartLimbo",
symbol:" £",
animationDivID:"totalEarningsTableAnimationLimbo",
eyeIconTitle: false,
eyeIconChart: false
};
createTableAndChartSection(document.getElementById("earningsTotalsTableSectionLimbo"), earningsTableTotalsElementsLimbo);
}
The createTotalsElements() passes two arguments to createTableAndChartSection() function, which creates and insert HTML elements into DOM. Previously I used an array to pass table parameters, but I switched to object now, as it is easier to read code in createTableAndChartSection() function (for example it’s easier to understand what attributeObject.symbol stands for then attributeObject[2]).
const createTableAndChartSection = (rowDivID, attributeObject) => {
//create table element.
let table = document.createElement("table");
table.setAttribute("class", "table");
//create caption with title and info icon
let caption = document.createElement("caption");
//title text
let titleText= document.createTextNode(attributeObject.title);
//add title text to caption
caption.appendChild(titleText);
//i element for info icon
//as not all tables have his icon need a condition to determine if it is necessary.
if (attributeObject.infoIconID !== false){
let iInfo = document.createElement("i");
iInfo.setAttribute("class", "fas fa-info-circle");
iInfo.setAttribute("data-toggle", "modal");
iInfo.setAttribute("data-target", "#infoModal");
iInfo.setAttribute("id", attributeObject.infoIconID);
//add i element to caption
caption.appendChild(iInfo);
}
//add caption to table
table.appendChild(caption);
//if this is true, create a heading for table.
if (attributeObject.appendThead){
let tHead = document.createElement("thead");
//create row
let tableRow = document.createElement("tr");
//table width 80% and 20% gets inherrited so no need to add these classess!
let tableHeading = document.createElement("th");
tableHeading.setAttribute("class", "col-xs-8");
tableHeading.textContent = "Name";
tableRow.appendChild(tableHeading);
let tableHeading2 = document.createElement("th");
tableHeading2.setAttribute("class", "col-xs-4");
tableHeading2.textContent = "Amount";
tableRow.appendChild(tableHeading2);
//add row to thead
tHead.appendChild(tableRow);
//add thead to table
table.appendChild(tHead);
}
//table row for 1 line of empty data
let tableRow = document.createElement("tr");
let tableData = document.createElement("td");
tableData.setAttribute("class", "col-xs-8");
tableData.textContent = "None";
tableRow.appendChild(tableData);
let tableData2 = document.createElement("td");
tableData2.setAttribute("class", "col-xs-4");
tableData2.textContent = "0"+attributeObject.symbol;
tableRow.appendChild(tableData2);
//create tbody element
let tableBody = document.createElement("tbody");
tableBody.setAttribute("id", attributeObject.tbodyID);
//add empty data table row to tbody
tableBody.appendChild(tableRow);
//add tbody to table
table.appendChild(tableBody);
//create nested div elements that will contain table
let div1 = document.createElement("div");
div1.setAttribute("class", "table-responsive");
div1.setAttribute("id", attributeObject.animationDivID);
//add table to div.
div1.appendChild(table);
//determine if canvas has to be added or not
if (attributeObject.renderChart){
//create tfoot element for canvas
let tfootCanvas = document.createElement("tfoot");
tfootCanvas.setAttribute("class", "hidden");
tfootCanvas.setAttribute("id", attributeObject.tfootID);
//create tr element for tfoot
let tableRowCanvas = document.createElement("tr");
//create td element for tr
let tableDataCanvas = document.createElement("td");
tableDataCanvas.setAttribute("colspan", "2");
//create canvas element for td
let canvas = document.createElement("canvas");
canvas.setAttribute("id", attributeObject.canvasID);
//nest elements
tableDataCanvas.appendChild(canvas);
tableRowCanvas.appendChild(tableDataCanvas);
tfootCanvas.appendChild(tableRowCanvas);
//add tfoot to table
table.appendChild(tfootCanvas);
}
//finally add all the created elements to the row in html file
rowDivID.appendChild(div1);
}
Once the HTML elements are created and added to the DOM, It’s time to load data into the tables and render charts. The loadDataTotals() function distributes data from back and to tables. It also passes HMTL elements as arguments that will be used to load data and calculate table height’s.
const loadDataTotals = (response) => {
let taxPeriodQuantityInputTotals = document.getElementById("taxPeriodQuantityTotals");
taxPeriodQuantityInputTotals.value = response.taxPeriodQuantity;
//First render pie charts, as they get added to the table, before calling loadTotalsTables it sets the table height
//as table height is calculated in the loadTotalsTables function,charts must be addd before calling it.
let totalsPieChartCheckValue = document.getElementById('totalsPieChartCheck').checked;
if (totalsPieChartCheckValue){
loadChartCounterTotals++;
//remove class hidden from charts
showTotalsCharts();
//if chart has been created previousley, must destroy it in order to load a new one
if (loadChartCounterTotals>1) {
//a function that destroys all totals charts.
destroyTotalsCharts();
}
//payments chart
paymentsPieChartTotals = renderPieChart(document.getElementById("totalEarningsChart"),response.paymentsTotalsChart);
//payments limbo chart
paymentsPieChartTotalsLimbo = renderPieChart(document.getElementById("totalEarningsChartLimbo"),response.paymentsTotalsChart);
} else {
//a function that adds class hidden to all charts.
hideTotalsCharts();
}
//these id values created in createTableAndChartSection function
//when loadTotasTables is called it fills main table and limbo table with the same data.
//payments
loadTotalsTables(
document.getElementById("totalEarningsTable"),
response.paymentsArrayForTable,
document.getElementById("totalEarningsTableLimbo"),
document.getElementById("totalEarningsTableAnimation"),
document.getElementById("totalEarningsTableAnimationLimbo"),
document.getElementById("totalEarningsChartDiv")
);
}
Now the loadTotalsTables() function does two things: loads data into tables and is responsible for gathering the values that are later passed to animateTable() function to animate the table. I have also added a fadeOut() and fadeIn() jQuery functions to flicker data being loaded, that indicate to the user that a change has happened.
const loadTotalsTables = (tableName, response, tableNameLimbo = false, animationContainer = false, animationContainerLimbo = false) => {
//to animate container that contains table and chart, value of initial height of the container is required
//then inside the while/do loop the tables height will be set to initial height on every iteration, to maintain the previous height
//meanwhile in the background the limbo table will be appended with new children to determine the new height for main table
//once the loop completes, the limbos table height will be picked and applied to animate the main table.
//if a chart is shown, it's height will have to be added separately to new height variable.
let initialHeight;
if (animationContainer) {
initialHeight = animationContainer.offsetHeight;
//empty limbo table
tableNameLimbo.innerHTML = "";
}
$(tableName).fadeOut(0);
//empty main table
tableName.innerHTML = "";
let parentArrayLength = Object.keys(response).length;
for (let x=0; x<parentArrayLength;x++){
let arrayLength = Object.keys(response[x]).length;
let i=0;
do {
let tableRow = document.createElement("tr");
//since the data is structured into multidimensional arrays, it is done so that it would be possible to determine when to add a
//top border to separate data in the table.
//if the parent array length is more then 1, it means that for every additional array inside a parent array, a top border must be added.
//so if x!=0 (means there are several sections of data to be added)
//and i== 0 (only add top border on first iteration ) --->
//if these two conditions are met a class that adds top border is added.
if (x!==0 && i== 0){
tableRow.setAttribute("class", "BT");
}
let tableData = document.createElement("td");
tableData.setAttribute("class", "col-xs-8");
tableData.textContent = response[x][i]['name'];
tableRow.appendChild(tableData);
let tableData2 = document.createElement("td");
tableData2.setAttribute("class", "col-xs-4");
tableData2.textContent = response[x][i]['earnings']+" "+response[x][i]['symbol'];
tableRow.appendChild(tableData2);
tableName.appendChild(tableRow);
//keep the old height on the animation container div.
if (animationContainer) {
animationContainer.style.height = initialHeight+"px"
}
//create and add the same data to the limbo table
//so that limbo's table height can later be used to animate main table.
if (animationContainer){
let tableRow = document.createElement("tr");
if (x!==0 && i== 0){tableRow.setAttribute("class", "BT");}
let tableData = document.createElement("td");
tableData.setAttribute("class", "col-xs-8");
tableData.textContent = response[x][i]['name'];
tableRow.appendChild(tableData);
let tableData2 = document.createElement("td");
tableData2.setAttribute("class", "col-xs-4");
tableData2.textContent = response[x][i]['earnings']+" "+response[x][i]['symbol'];
tableRow.appendChild(tableData2);
tableNameLimbo.appendChild(tableRow);
}
i++;
}
while (i < arrayLength);
}
$(tableName).fadeIn(2000);
//animate table height
if (animationContainer) {
//grab the height of limbo table, which will be the new height for the main table
let newHeight = animationContainerLimbo.offsetHeight;
//call the animation function and pass the new height for the table.
animateTableHeight(animationContainer, newHeight);
}
}
Once the data is loaded into tables, animateTableHeight() function is called where as parameters are passed the table container to be animated and it’s new height.
let animateTableHeightDurationTest = 751;
const animateTableHeight = (table, height) => {
height = height+"px";
$(table).animate({height: height},{
queue : false,
duration : animateTableHeightDurationTest,
complete : function() {
}});
}
As I intend to use the same functions on main page tables and charts, they have some parameters that are not used in these examples. And in main page I have about 15 tables and charts and doing the back end and animations will take a lot of time… 🙁 but I really want to get it done properly! 😉