With the rest of the linting, I am told that there are 13 issues remaining. But that can be misleading where other types of things are found after the current lot are taken care of.
Here’s the JS code as it stands right now:
jQuery(document).ready(function rankWeather($) {
var StatJSON = {
"Option1": {
"ColHeading": "New York",
"Ranking": "",
"Rating": "4.5",
"Row1Heading": "Hello",
"Weather": "40d71n74d01/new-york"
},
"Option2": {
"ColHeading": "San Francisco",
"Ranking": "",
"Rating": "3.2",
"Row1Heading": "Whats",
"Weather": "37d77n122d42/san-francisco"
},
"Option3": {
"ColHeading": "Chicago",
"Ranking": "",
"Rating": "3.7",
"Row1Heading": "Up",
"Weather": "41d88n87d63/chicago"
},
"Option4": {
"ColHeading": "Los Angeles",
"Ranking": "",
"Rating": "5.0",
"Row1Heading": "With",
"Weather": "34d05n118d24/los-angeles"
}
};
function printTable(data) {
var html = `<table class="compTable"><thead><tr><th>`;
if (data && data.length) {
html += "</th>";
$.each(data, function addColumnHeading(i) {
html += "<th>" + StatJSON[data[i]].ColHeading + "</th>";
});
html += "</tr>";
html += "<tbody>";
$.each(StatJSON[data[0]], function addRow(rowTitle) {
html += "<tr>";
if (rowTitle !== "ColHeading") {
html += "<td>" + rowTitle + "</td>";
}
$.each(data, function addData(cityInfo) {
if (rowTitle !== "ColHeading") {
var cityData = StatJSON[data[cityInfo]][rowTitle];
html += "<td>" + cityData + "</td>";
}
});
html += "</tr>";
});
} else {
html += "No results found</td></tr>";
}
html += "</tbody></table>";
return html;
}
function updateWeatherTable() {
var getCity = (option) => option.value;
var data = $("#selection").find(":selected").toArray().map(getCity);
$("#divResult").empty().append(printTable(data));
}
function cleanupTextAndClasses() {
var headings = $(".divResult table th:not(:first)");
headings.toArray().forEach(function(header) {
var columnHead = $(header).text().split(" ").join("");
$(header).addClass($(header).text().split(" ").join(""));
});
$(".divResult table th:first-child").removeAttr("name");
$("table thead th[class]").each(function(index, th) {
$(`table tbody td:nth-child(${index + 1})`).addClass(th.className);
});
var firstCells = $(".divResult tbody td:first-child");
firstCells.toArray().forEach(function(cell) {
$(cell).addClass(((cell.textContent).replace("#", "").replace(/[()]/g, "")).replace(/s/g, "").replace(/\\|\//g, ""));
$(cell).siblings("td").addClass(((cell.textContent).replace("#", "").replace(/[()]/g, "")).replace(/s/g, "").replace(/\\|\//g, ""));
$(cell).parent("tr").addClass(((cell.textContent).replace("#", "").replace(/[()]/g, "")).replace(/s/g, "").replace(/\\|\//g, ""));
});
}
function updateRatings() {
function updateRating(cell) {
var rating = parseFloat($(cell).text()).toFixed(2);
$(cell).html(`<div><div class="rating">${rating}</div></div>`);
}
function updateRowRatings(ratingCell) {
$(ratingCell).nextAll("td").toArray().forEach(updateRating);
}
$(".divResult .Rating").toArray().forEach(updateRowRatings);
}
function updateRankings() {
function rankByRatings() {
var ratings = [];
$("tr.Rating > td:not(:first)").toArray().forEach(function(td) {
var element = $(td).text();
ratings.push(element);
});
var sorted = ratings.slice().sort(function(a, b) {
return b - a;
});
var ranks = ratings.slice().map(function(v) {
return sorted.indexOf(v) + 1;
});
return ranks;
}
function sortColumnsByRanking() {
var Rows = $(".compTable tr");
var RowRanking = $(".compTable tr.Ranking");
RowRanking.find("td:not(:first)").sort(function(a, b) {
return $(a).text().replace(/[^0-9]/gi, "").localeCompare($(b).text().replace(/[^0-9]/gi, ""));
}).each(function(new_Index, cell) {
var original_Index = $(cell).index();
Rows.toArray().forEach(function(row) {
var td = $(row).find("td, th");
if (original_Index !== new_Index) {
td.eq(original_Index).insertAfter(td.eq(new_Index));
}
});
});
}
function updateRanking(cell, rank) {
cell.empty().append($(`<div><div "ranking">#${rank}</div></div>`));
}
var ranks = rankByRatings();
$(".divResult table tbody tr").toArray().forEach(function(tr) {
var $td = $(tr).children();
if ($td.eq(0).text() === "Ranking") {
ranks.forEach(function(rank, i) {
updateRanking($td.eq(i + 1), rank);
if (rank === 1) {
$td.eq(i + 1).addClass("ranking1");
}
});
}
});
sortColumnsByRanking();
}
function addWeatherWidget() {
function showWeatherWidget(el) {
var path = $(el).text();
var linkHtml = `<a href="https://forecast7.com/en/${path}/"`;
linkHtml += ` class="weatherwidget-io" data-label_2="WEATHER"`;
linkHtml += ` data-days="3" data-theme="original">WEATHER</a>`;
$(el).html(linkHtml);
}
$("#divResult table tbody tr td").toArray().forEach(function(td) {
if ($(td).text() === "Weather") {
$(td).nextAll("td").toArray().forEach(showWeatherWidget);
}
__weatherwidget_init();
});
}
$("#btnSubmit").click(function citiesSubmitHandler() {
updateWeatherTable();
cleanupTextAndClasses();
updateRatings();
updateRankings();
addWeatherWidget();
});
});
Most of the remaining issues are about naming functions.
Linting the column class function
Here’s the start of the cleanupTextAndClasses function
var headings = $(".divResult table th:not(:first)");
headings.toArray().forEach(function(header) {
var columnHead = $(header).text().split(" ").join("");
$(header).addClass($(header).text().split(" ").join(""));
});
Calling the function addHeadingClass seems to work well. There is a columnHead variable that’s not used though, so we can fix that up while we’re here.
var headings = $(".divResult table th:not(:first)");
headings.toArray().forEach(function addHeadingClass(header) {
var columnHead = $(header).text().split(" ").join("");
$(header).addClass(columnHead);
});
Linting the cell class code
This next piece of code seems weird.
$("table thead th[class]").each(function(index, th) {
$(`table tbody td:nth-child(${index + 1})`).addClass(th.className);
});
It’s supposed to copy the class name of the heading down to each of the table cells below that heading, but that doesn’t seem to happen. Instead, with New York and Los Angeles selected for example, it is the far left column with the row titles that has the classes New York added to the cells in that far left column.
The problem is that the index is being incorrectly calculated.
The headings have an index of 0 and 1. Adding 1 to those gives us 1 and 2, but nth-child starts at 1, so 1 is going to give you the first column with the row titles. As a result you need to add an extra 1 to get to the correct column for the headings.
That’s too much complication. If we instead remove the [class]
part of the selector, the we can use a simple +1 to get to the correct column instead.
$("table thead th").each(function propagateHeadingClass(index, th) {
$(`table tbody td:nth-child(${index + 1})`).addClass(th.className);
});
I’ve also gone with calling the function propagateHeadingClass, because we take the heading class and we copy it to all of the cells that are below it in the same column.
Linting the propagate row class code
Here is the next piece of code that we’re linting.
var firstCells = $(".divResult tbody td:first-child");
firstCells.toArray().forEach(function(cell) {
$(cell).addClass(((cell.textContent).replace("#", "").replace(/[()]/g, "")).replace(/s/g, "").replace(/\\|\//g, ""));
$(cell).siblings("td").addClass(((cell.textContent).replace("#", "").replace(/[()]/g, "")).replace(/s/g, "").replace(/\\|\//g, ""));
$(cell).parent("tr").addClass(((cell.textContent).replace("#", "").replace(/[()]/g, "")).replace(/s/g, "").replace(/\\|\//g, ""));
});
Before deciding on a function name, I’ll clean up some of the code so that I end up with a better idea of what it does.
All of that replacing code can be moved out to a separate function. The replacing code sanitizes the text, so I’ll call it sanitize. Now that I understand what is being done there more clearly, the looping function can be called propagateRowClass.
var firstCells = $(".divResult tbody td:first-child");
firstCells.toArray().forEach(function propagateRowClass(cell) {
function sanitize(text) {
text = text.replace("#", "");
text = text.replace(/[()]/g, "");
text = text.replace(/s/g, "");
text = text.replace(/\\|\//g, "");
return text;
}
var className = sanitize(cell.textContent);
$(cell).addClass(className);
$(cell).siblings("td").addClass(className);
$(cell).parent("tr").addClass(className);
});
That works, but I want to update the sanitize function so that it has each of those regular expressions in an array, and uses the reduce method to apply each of them to the text. That means first placing each regular expression into an array:
function sanitize(text) {
var removals = ["#", /[()]/g, /s/g, /\\|\//g];
text = text.replace(removals[0], "");
text = text.replace(removals[1], "");
text = text.replace(removals[2], "");
text = text.replace(removals[3], "");
return text;
}
Now that those replace lines are all identical but for the array index, I can use a reduce method to apply each array item to the text.
function sanitize(text) {
var removals = ["#", /[()]/g, /s/g, /\\|\//g];
return removals.reduce(function sanitizeText(text, regex) {
return text.replace(regex, "");
}, text);
}
var className = sanitize(cell.textContent);
Do I now need that sanitize function? Let’s see how things look without it.
var firstCells = $(".divResult tbody td:first-child");
firstCells.toArray().forEach(function propagateRowClass(cell) {
var removals = ["#", /[()]/g, /s/g, /\\|\//g];
var className = removals.reduce(function sanitizeText(text, regex) {
return text.replace(regex, "");
}, cell.textContent);
$(cell).addClass(className);
$(cell).siblings("td").addClass(className);
$(cell).parent("tr").addClass(className);
});
I’m okay with that. What I’m not okay with though is that the same classname is being copied across all of the cells of row, and on the row element itself too. Normally only one classname is placed on the row, as its styles tend to apply to everything inside of the row too. I’ll leave things as they are with the row title being propagated as a classname to the row and to all of the cells in that row, but this is something for you to consider. Only one class on the row itself is all that is normally needed.
Linting the retrieval of ratings
The next code that the linter says needs a function name is the following:
var ratings = [];
$("tr.Rating > td:not(:first)").toArray().forEach(function(td) {
var element = $(td).text();
ratings.push(element);
});
I’ve been noticing that there are a lot of adjustments in the code for the first column not being used by a city. Did you know that tables let you use a TH element on the row? That makes things a lot easier because only the td rows will be of interest.
Aside from that potential improvement for later on, adding items to the array means that a map is more suitable than using forEach.
var ratingCells = $("tr.Rating > td:not(:first)");
var getText = (td) => $(td).text();
var ratings = ratingCells.toArray().map(getText);
The idea of getting the rankings is important enough to move that code into a separate function too.
Linting the sorting of the rankings
Here’s the sorting part of the rankByRatings function:
var sorted = ratings.slice().sort(numericSort);
var ranks = ratings.slice().map(function(v) {
return sorted.indexOf(v) + 1;
});
return ranks;
The sort function and the map function are wanting names.
With the sort function, I’ll just call that numericSort and move it to be above the rankByRatings function. As for the map function, I’ll call that decideRank.
function numericSort(a, b) {
return b - a;
}
...
var sorted = ratings.slice().sort(numericSort);
var ranks = ratings.slice().map(function decideRank(ratingIndex) {
return sorted.indexOf(ratingIndex) + 1;
});
return ranks;
The slice of the ranks isn’t needed. It is wanted for sorted because of how sort changes the array, but with the map, slice is of no use at all, and can be removed.
Lastly, because we have the function called decideRank to help inform is about what’s going on, we can extract the decideRank function and instead of assigning the map to a ranks variable, we can just return the map.
The rankByRatings function is now reduced to be the following:
function rankByRatings() {
var ratings = getRatings();
var sorted = ratings.slice().sort(numericSort);
return ratings.map(function decideRank(ratingIndex) {
return sorted.indexOf(ratingIndex) + 1;
});
}
Linting sort rankings
The next three sets of functions that want names for them, is in the sortColumnsByRanking function.
function sortColumnsByRanking() {
var Rows = $(".compTable tr");
var RowRanking = $(".compTable tr.Ranking");
RowRanking.find("td:not(:first)").sort(function(a, b) {
return $(a).text().replace(/[^0-9]/gi, "").localeCompare($(b).text().replace(/[^0-9]/gi, ""));
}).each(function(new_Index, cell) {
var original_Index = $(cell).index();
Rows.toArray().forEach(function(row) {
var td = $(row).find("td, th");
if (original_Index !== new_Index) {
td.eq(original_Index).insertAfter(td.eq(new_Index));
}
});
});
}
The sorting function can reuse the numericSort that we had before. There shouldn’t be all of that activity in the sort function. Instead, we can use map to clean up the text to numbers, and then sort them.
function toNumber(el) {
var digits = $(el).text().replace(/[^0-9]/gi, "");
return Number(digits);
}
var rankings = RowRanking.find("td:not(:first)").map(toNumber);
Linting reorder columns
Just after sorting the rankings, is reordering the columns.
function sortColumnsByRanking() {
var Rows = $(".compTable tr");
var RowRanking = $(".compTable tr.Ranking");
...
rankings.sort(numericSort).each(function(new_Index, cell) {
var original_Index = $(cell).index();
Rows.toArray().forEach(function(row) {
var td = $(row).find("td, th");
if (original_Index !== new_Index) {
td.eq(original_Index).insertAfter(td.eq(new_Index));
}
});
});
Underscores don’t tend to belong in JS variables, so I’ve removed those, and the Rows variable shouldn’t be out of the map function, so it gets moved in to the map function as a lowercase rows instead.
When it comes to naming the functions, I don’t know what to call the map function yet, so I’ll work on the forEach function instead to gain a better understanding of what happens there.
The name swapColumns seems to explain well what happens in the forEach code. I will though move the comparision up to the top of the function, so that an early return can be done when both indexes are the same. That way the comparison acts as what is called a Guard Clause, preventing the execution of code from progressing any further unless the conditions are met.
var originalIndex = $(cell).index();
rows.toArray().forEach(function swapColumns(row) {
if (originalIndex === newIndex) {
return;
}
var td = $(row).find("td, th");
td.eq(originalIndex).insertAfter(td.eq(newIndex));
});
Actually, that guard clause can be moved further up in the code, so that the forEach doesn’t even occur if it doesn’t have to.
var originalIndex = $(cell).index();
rows.toArray().forEach(function swapColumns(row) {
if (originalIndex === newIndex) {
return;
}
var td = $(row).find("td, th");
td.eq(originalIndex).insertAfter(td.eq(newIndex));
});
I now feel comfortable enough with the code that I can provide a suitable name for the each function, which is to call it moveColumn.
var RowRanking = $(".compTable tr.Ranking");
var rankings = RowRanking.find("td:not(:first)").map(toNumber);
rankings.sort(numericSort).each(function moveColumn(newIndex, cell) {
...
});
The only problem is that the linter isn’t happy about the length of the line. That’s alright, for it seems that the code will make better sense if the map and the sort are kept together on the same line.
var RowRanking = $(".compTable tr.Ranking").find("td:not(:first)");
var rankings = RowRanking.map(toNumber).sort(numericSort);
rankings.each(function moveColumn(newIndex, cell) {
There are two more sets of function names to go, according to the linter.
Linting showRankings function
Here’s the showRankings function where the forEach functions need naming.
function showRankings(ranks) {
function updateRanking(cell, rank) {
cell.empty().append($(`<div><div "ranking">#${rank}</div></div>`));
}
$(".divResult table tbody tr").toArray().forEach(function(tr) {
var $td = $(tr).children();
if ($td.eq(0).text() === "Ranking") {
ranks.forEach(function(rank, i) {
updateRanking($td.eq(i + 1), rank);
if (rank === 1) {
$td.eq(i + 1).addClass("ranking1");
}
});
}
});
}
First observation is that there is a large arrow of indenting. We can easily improve that by extracting the functions, but that means naming them first.
The inner forEach function needs a name, but it’s hard to think of one other than updateRanking. That means that the code isn’t different enough, and should be in the same function. I moved some code out to a separate updateRanking function to help cope with too-long lines. I will temporarily inline that updateRanking function, so that more of the code can late ron be extracted out
function showRankings(ranks) {
$(".divResult table tbody tr").toArray().forEach(function(tr) {
var $td = $(tr).children();
if ($td.eq(0).text() === "Ranking") {
ranks.forEach(function (rank, i) {
$td.eq(i + 1).empty().append($(`<div><div "ranking">#${rank}</div></div>`));
if (rank === 1) {
$td.eq(i + 1).addClass("ranking1");
}
});
}
});
}
The inner forEach function can now be extracted out fully, as an updateRanking function.
function showRankings(ranks) {
function updateRow(tr) {
function updateCell(rank, i) {
var html = $(`<div><div "ranking">#${rank}</div></div>`);
$td.eq(i + 1).empty().append(html);
if (rank === 1) {
$td.eq(i + 1).addClass("ranking1");
}
}
var $td = $(tr).children();
if ($td.eq(0).text() === "Ranking") {
ranks.forEach(updateCell);
}
}
$(".divResult table tbody tr").toArray().forEach(updateRow);
}
There is some other work that I want to do with that code, such as to separate apart the collection of data and using that data to update the table. But that is something that can wait until after the linting is finished.
Linting addWeatherWidgets
Here the code that we currently have for addWeatherWidgets
function addWeatherWidgets() {
function showWeatherWidget(el) {
var path = $(el).text();
var linkHtml = `<a href="https://forecast7.com/en/${path}/"`;
linkHtml += ` class="weatherwidget-io" data-label_2="WEATHER"`;
linkHtml += ` data-days="3" data-theme="original">WEATHER</a>`;
$(el).html(linkHtml);
}
$("#divResult tbody tr td").toArray().forEach(function(td) {
if ($(td).text() === "Weather") {
$(td).nextAll("td").toArray().forEach(showWeatherWidget);
}
__weatherwidget_init();
});
}
The forEach function can be extracted out to showWeatherWidgets (plural instead of singular), and the __weatherwidget_init() can be moved out of the loop. That way, you don’t end up with the weather attempting to load multiple times.
function addWeatherWidgets() {
function showWeatherWidget(el) {
var path = $(el).text();
var linkHtml = `<a href="https://forecast7.com/en/${path}/"`;
linkHtml += ` class="weatherwidget-io" data-label_2="WEATHER"`;
linkHtml += ` data-days="3" data-theme="original">WEATHER</a>`;
$(el).html(linkHtml);
}
function showWeatherWidgets(td) {
if ($(td).text() === "Weather") {
$(td).nextAll("td").toArray().forEach(showWeatherWidget);
}
}
$("#divResult tbody tr td").toArray().forEach(showWeatherWidgets);
__weatherwidget_init();
}
That’s all for the naming of functions. The linter still has a few things to say about moving var statements up to the top of functions, but it’s going to be the next post where I next take a look at that.