WeatherWidget.io - Uncaught DOMException: Failed to execute 'removeChild' on 'Node' issue

I have a dynamic table with weather of cities being populated with the ‘weather-widget.io’. I am using the below code to populate the “Weather” row.

jQuery('#divResult table tbody tr td').each(function ($) {
        if (jQuery(this).text() == 'Weather') jQuery(this).nextAll("td").each(function ($) {
            jQuery(this).html('<a class="weatherwidget-io" href="https://forecast7.com/en/' + jQuery(this).text() + '/" data-label_2="WEATHER" data-days="3" data-theme="original" >WEATHER</a>');
        });__weatherwidget_init()
    });

This works fine by itself. However, I have another line of code which reorders the columns of the table as per the ranking it gets from the ‘Rating’ field.

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) {
            var original_Index = $(this).index();

            Rows.each(function() {
            var td = $(this).find('td, th');
            if (original_Index !== new_Index)
            td.eq(original_Index).insertAfter(td.eq(new_Index));
            });
        });

This rearranges the columns as per the ‘Ranking’ as required, however it breaks the “__weatherwidget_init()” and gives me the ‘Uncaught DOMException: Failed to execute ‘removeChild’ on ‘Node’: The node to be removed is not a child of this node.’ error.

How do I avoid the ‘Uncaught DOMException’ error in this case as I still need the columns ordered as per Ranking?

Find below working code: https://jsfiddle.net/mithunu/8dpvjuf1/2/

(P.S. For some reason the error doesn’t show in the jsfiddle console but it does appears in the chrome/firefox browser console. The error also interferes with the ‘weather’ display in ‘mobile view’ mode when I use an Responsive jquery plugin like ‘Restable’.)

I’ve attempted to experience your problem but when I select a city and submit to get the weather info, no such error occurs in either my Chrome or Firefox browser consoles.

Hi Paul, good to see you again!! Many thanks for trying it out in the browser for me. Unfortunately, I’m still getting the error as you can see in the screenshot below. Would be very grateful if you could share the code that you are running, which isn’t giving you this error, so that I can try it at my end too. I’m probably doing something wrong here which is giving me this error.

What I did to explore the issue is to start from the working code at https://jsfiddle.net/mithunu/8dpvjuf1/2/ to which I updated the code using the code that you supplied in your post. That code seems to be identical to what is in the working code. After that updated code is rerun, I select a city from the select list and submit it to get the weather.

  • Do you get the problem only from the working code? I don’t for some reason.
  • Is there a reason for you calling it “working code” when it doesn’t seem to work for you?
  • Is there something else that I should do to try and experience your same problem?

Hi Paul,
Thanks for all the efforts taken. :slight_smile: Find attached the .html file with the same code.
WeatherWidgetError.html (6.1 KB)

Please open this and check the console. Would like to know if your still not getting the error at your end, in which case the problem might be something else entirely.
Thanks again!!

In Chrome I am told that “Page layout may be unexpected due to Quirks Mode” which is easily dealt with by using a !DOCTYPE tag above the HTML tag.

<!DOCTYPE html>
<html>
...
</html>

The above comes from the list of [Recommended Doctype Declarations]( Recommended Doctype Declarations).

In Firefox I am also told that “The character encoding of the HTML document was not declared.”

The Declaring character encodings in HTML page says to use:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>

I still don’t get any other errors though in relation to your Uncaught DOMException.

Have you tried checking if an extension is causing the trouble, by starting Firefox in troubleshooting mode?

Hi Paul!! Thanks!! Are you sure you are not getting these errors even after selecting options and clicking on submit?? Like below screenshot??
If not, then I guess the problem appears to be only at my end. Will try troubleshooting for errors.
Thanks again!! :slight_smile:

I’ve only tried selecting single options so far. Will attempt multiple options right now.

I have managed to experience your same problem. The vital information to achieve your issue is that multiple cities must be selected first, before submitting them.

Further work can now occur to troubleshoot the problem, now that we know how to reliably experience it.

1 Like

Yes, the problem appears only with multiple options and not a single one. My bad, I must have checked and clarified that.

I guess it doesn’t happen in case of a single option is because the problem is due to the ‘column rearrange as per ranking’ which doesnt kick-in in case of a single option selection.

The problem is due to this piece of code below, which rearranges the columns of the table, but not sure how I treat it as I need still my table rearranged as per ranking.

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) {
        var original_Index = $(this).index();

        Rows.each(function() {
        var td = $(this).find('td, th');
        if (original_Index !== new_Index)
        td.eq(original_Index).insertAfter(td.eq(new_Index));
        });
    });

Now that we know what causes the problem, I want the webpage to automatically load with the problem happening.

I add the following code to select the top two options, and click the submit button. The submit button doesn’t want to click until some time after the page has loaded, so putting the click in a setTimeout is a suitable solution for that.

function loadDefaultWeather() {
    const selection = document.querySelector("#selection");
    selection.options[0].selected = true;
    selection.options[1].selected = true;

    setTimeout(function () {
        const submit = document.querySelector("#btnSubmit");
        submit.click();
    });
}
window.addEventListener("load", loadDefaultWeather);

Now when the page loads, it automatically selects the first two options and submits them to to get the weather.

But, that all looks to work well. No console browser errors occur at all.

It’s time to try and chart what works and what doesn’t.

  • New York :heavy_check_mark:
  • San Francisco :heavy_check_mark:
  • Chicago :heavy_check_mark:
  • Los Angeles :heavy_check_mark:
  • New York & San Francisco :heavy_check_mark:
  • New York & Chicago :heavy_check_mark:
  • New York & Los Angeles :x:
  • San Francisco & Chicago :x:
  • San Francisco & Los Angeles :x:
  • Chicago & Los Angeles :x:
  • New York & San Francisco & Chicago :x:
  • New York & San Francisco & Los Angeles :x:
  • New York & Chicago & Los Angeles :x:
  • San Francisco & Chicago & Los Angeles :x:

It was only by luck that I found that the NY&SF and NY&C also work.

From that, the simplest situation that doesn’t work is NW&LA, so the updated code that guarantees a failure on loading the page is:

function loadDefaultWeather() {
    const selection = document.querySelector("#selection");
    selection.options[0].selected = true;
    selection.options[3].selected = true;

    setTimeout(function () {
        const submit = document.querySelector("#btnSubmit");
        submit.click();
    });
}
window.addEventListener("load", loadDefaultWeather);

Now that we have a guaranteed way to cause the problem, investigation can occur from here about what is causing the trouble to occur.

Gathering further information about the problem, when New York and Los Angeles are both selected and submitted to get the weather, the following section of code is where the trouble occurs.

        if (original_Index !== new_Index)
          td.eq(original_Index).insertAfter(td.eq(new_Index));

Even when there is only one statement in an if statement, it’s beneficial to have the braces around that single statement.

        if (original_Index !== new_Index) {
          td.eq(original_Index).insertAfter(td.eq(new_Index));
        }

This is partly because it helps to aid in terms of recognition, and secondly because you never know when you’re going to need more than one thing happening in the if statement.

In that if statement we can add a console.log to help us see what’s happening.
I’ll comment out the insertAfter too, so that execution doesn’t end and we can see more console.log information about things.

        if (original_Index !== new_Index) {
          console.log({new_Index, td, el: td.eq(new_Index)})
          // td.eq(original_Index).insertAfter(td.eq(new_Index));
        }

The information we gain from that is:

{new_Index: 0, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 0, td: r.fn.init(0), el: r.fn.init(0)}
{new_Index: 0, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 0, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 0, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 0, td: r.fn.init(3), el: r.fn.init(1)}

For one of those situations, td.eq(new_Index) gives no element. Is that what causes the problem though?

Enabling the commented out line lets us see information before the error occurs:

        if (original_Index !== new_Index) {
          console.log({new_Index, td, el: td.eq(new_Index)})
          td.eq(original_Index).insertAfter(td.eq(new_Index));
        }

We now learn some unexpected information, that all of the console.log statements occur before the error takes place.

{new_Index: 0, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 0, td: r.fn.init(0), el: r.fn.init(0)}
{new_Index: 0, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 0, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 0, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 0, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 1, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 1, td: r.fn.init(0), el: r.fn.init(0)}
{new_Index: 1, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 1, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 1, td: r.fn.init(3), el: r.fn.init(1)}
{new_Index: 1, td: r.fn.init(3), el: r.fn.init(1)}
widget.min.js:1 Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
9 widget.min.js:1 Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

Is is really the last item that causes the error? Or is that because of some funny business with jQuery that delays the insertAfter action. I’m going with funny business.

Let’s investigate that insertAfter more closely, by gaining information about the elements being used.

        if (original_Index !== new_Index) {
          console.log({
            originalEl: td.eq(original_Index)[0],
            newEl: td.eq(new_Index)[0]
          });
          // td.eq(original_Index).insertAfter(td.eq(new_Index));
        }

We now get console.log information about the elements being used for insertAfter.

{originalEl: th.losangeles, newEl: th}
{originalEl: undefined, newEl: undefined}
{originalEl: td.losangeles.ranking, newEl: td.ranking}
{originalEl: td.losangeles.rating, newEl: td.rating}
{originalEl: td.losangeles.row1heading, newEl: td.row1heading}
{originalEl: td.losangeles.weather, newEl: td.weather}

The undefined really seems to be the cause of the problem, so yes it was jQuery funny business giving misleading console.log information about the timing of events.

It is entirely possible to just check if an element exists before doing the insertAfter, but I want to also understand more about what’s causing the problem.

Let’s replace that insertAfter with a function that does the insert, so that we can remove any confusion from jQuery about the situation.

    function insertAfter(newNode, existingNode) {
      const parent = existingNode.parentNode;
      parent.insertBefore(newNode, existingNode.nextSibling);
    }
...
          if (original_Index !== new_Index) {
            console.log(td.eq(original_Index)[0]);
            // td.eq(original_Index).insertAfter(td.eq(new_Index));
            insertAfter(td.eq(original_Index)[0], td.eq(new_Index)[0]);
          }

We can now update the insertAfter function to return early if there is no existingNode element.

    function insertAfter(newNode, existingNode) {
      if (!newNode|| !existingNode) {
          return;
      }
      const parent = existingNode.parentNode;
      parent.insertBefore(newNode, existingNode.nextSibling);
    }

Even after doing all of that to ensure that the nodes exist, we still get a console.log error.

<td class=​"NewYork Rating">​…​</td>​
<td class=​"NewYork Row1Heading">​Hello​</td>​
<td class=​"NewYork Weather">​…​</td>​
Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.
9widget.min.js:1 Uncaught DOMException: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node.

We have now confirmed that the jQuery insertAfter that was in the code, is not responsible for the problem. I was unjustly slighting jQuery. What really is happening is that after we reorganise the code, the weather widget is trying to update something and can’t find it.

Does that mean that we should wait until after the weather widget has done things, before moving things around?

I’ll remove all of those insertAfter changes and instead put the original rows code into a setTimeout for 5 seconds.

    setTimeout(function () {
        console.log("reorder rows");
        var Rows = $('.compTable tr');
        ...
    }, 5000);

That works perfectly with no errors.

The cause of the problem is that we are messing around with the HTML structure, while the weather widget is in the middle of updating things.

Next up is how to solve this without needing a 5 second delay.

Hi Paul,

Extremely grateful for all the trouble you have taken and the detailed approach and explanation you have provided. :cowboy_hat_face: Learnt a lot there!!

The setTimeout approach is a nice and simple way to escape the error. Really looking forward to know how to solve it without needing a 5 second delay.

Unfortunately, even though the DOMException error is no longer coming in the console, the weatherwidget iframe is not launching in the ‘responsive’ mobile view. (I’m using the “ReStable” jquery plugin (https://www.jqueryscript.net/table/Lightweight-jQuery-Responsive-Table-Plugin-ReStable.html) to convert the table to ul-li format for mobile browsing. Hopefully, the “without-5-second-delay-approach” you are referring to resolves that issue.) :slight_smile:

Once agin, extremely grateful for all the trouble taken and looking forward to your replies!! :slight_smile:

I’ve been getting too distracted with linting and improving your own code, when I should be focusing on the weather widget instead.

The weather widget code doesn’t provide any way to be notified when it is finished doing its thing. As a result, any further benefit occurs from making changes to the weather widget code so that we can be notified when it’s finished doing its thing. Are you okay with the possibility of that being done to make further progress?

Yes Paul, I’m sorry about that. Perfectly fine with your approach. Thanks again, for all your patience with me. :grin:

1 Like

Somehow after all of my editing of your code I can’t get the error to appear again, with no timeout.

There is also the possibility to attach on to a message sent from the weather iframe to the main window, to help us figure out when to do the updating.

I think that I have to do a large post about the linting and other updates that I made to your code, as I try to figure out what I did to fix things.

It’s time to lint using the harshest linter that I know of, JSLint.

  • trailing commas after the last item are removed
  • single quotes replaced with either double quotes or when double quotes are already i the text, backtick quotes
  • the this keyword is replaced with a local variable
  • braces are put around if statements with a single statement block
  • for loops are replaced with forEach loops

While linting I find oddities such as the following:

jQuery(".divResult table th:first-child").removeClass;
jQuery(".divResult table th:first-child").removeAttr("name");

I’m pretty sure that the removeClass is leftover from earlier code, and isn’t needed anymore, as it is a th element with no class attributes, so I’ve removed the removeClass line.

Other lintings are:

  • === used instead of ==
  • PrintTable function is out of scope, so move it up to the top of the code
  • rename it printTable instead. Capital first letters are for new FuncName() instantiations instead

Different data variables are used in different places, in the printTable function, in the button submit function, and in code further down that does row ranking. That last situation can see the data from the middle situation, so I’ve renamed data from that last situation to be ratings instead.

    var ratings = [];
    jQuery("tr.Rating > td:not(:first)").each(function(ignore, td){
    var element = jQuery(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;
    });

That only leaves lots of linting comments about lines being too long. The first one is the following:

jQuery("table thead th[class]").each(function (ignore, th) {
    $("table tbody td:nth-child(" + ($(th).index() + 1) + ")").addClass(th.className);
});

I’ll head back after the linting to take care of many things, including that ignore variable. JSLint doesn’t like it when a function parameter isn’t used, and so requires ignore to be used. I prefer to adjust techniques so that ignore isn’t needed at all.

With the above code, I can just use an index parameter, and use that instead of having jQuery calculate it.

jQuery("table thead th[class]").each(function (index, th) {
    $(`table tbody td:nth-child(${index + 1})`).addClass(th.className);
});

The next line that's too long is:
jQuery(".divResult table tbody tr").find("td:first").each(function (ignore, td) {

We can assign the table row to a variable, and use that to help deal with that issue.

var firstCells = jQuery(".divResult table tbody tr").find("td:first");
firstCells.each(function (ignore, cell) {

However, I also want to deal with that ignore situation. jQuery got in early with their implementation of each and got it completely backwards by using (index, item) for the parameters. They weren’t able to change it when web browsers standardised on using (item, index) instead.

Because of that, I find that better results are obtained by using the normal JavaScript forEach method (and associated ones such as map, filter, reduce, etc). In this case a simple forEach will do.

var firstCells = document.querySelectorAll(".divResult tbody td:first-child");
firstCells.forEach(function (cell) {

The rest of the function is a bit of a nightmare, but we can take care of that after the linting.


The next code with a too-long line is:

jQuery(".divResult table tbody tr td").each(function (ignore, td) {
    if (jQuery(td).text() === "Rating") {
        jQuery(td).nextAll("td").each(function (ignore, cell) {
            jQuery(cell).html(`<div style="display:inline-block; width:100%;"><div class="rating" style="background-color:#558000">` +  parseFloat(jQuery(cell).text()).toFixed(2) + "</div></div>");
        });
    }
});

That code has a characteristic “arrow formation” to its indenting which is a clear sign that the code can be structured more appropriately.
But the main culprit is that the styles really shouldn’t be in the JavaScript code and are much more suited being in a stylesheet instead. I see that the HTML page already has a stylesheet. We can add those styles to the HTML page instead.

td>div {
    display:inline-block;
    width:100%;
}
.rating {
    background-color:#558000;
}

The divResult selector can be shortened somewhat because table and tr don’t need to be specified to get the td elements. We can also work out the rating before adding it to the page, which helps to shorting things nicely.

jQuery(".divResult tbody td").each(function findRating(ignore, td) {
    if (jQuery(td).text() === "Rating") {
        jQuery(td).nextAll("td").each(function updateRating(ignore, cell) {
            var rating = parseFloat(jQuery(cell).text()).toFixed(2);
            jQuery(cell).html(`<div><div class="rating">${rating}</div></div>`);
        });
    }
});

We can then extract out those named functions:

function updateRating(ignore, cell) {
    var rating = parseFloat(jQuery(cell).text()).toFixed(2);
    jQuery(cell).html(`<div><div class="rating">${rating}</div></div>`);
}
function findRating(ignore, td) {
    if (jQuery(td).text() === "Rating") {
        jQuery(td).nextAll("td").each(updateRating);
    }
}
jQuery(".divResult tbody td").each(findRating);

But that’s not good enough, as the ignore function parameters need to be removed. That’s done by replacing the each jQuery method with the forEach method instead.

function updateRating(ignore, cell) {
    var rating = parseFloat(jQuery(cell).text()).toFixed(2);
    jQuery(cell).html(`<div><div class="rating">${rating}</div></div>`);
}
function findRating(td) {
    if (jQuery(td).text() === "Rating") {
        jQuery(td).nextAll("td").toArray().forEach(updateRating);
    }
}
document.querySelectorAll(".divResult tbody td").forEach(findRating);

But that’s not good enough, for the findRating function that searches all over the table for the text of “Rating” can be replaced with just a search for the “Rating” class name instead.

function updateRating(cell) {
    var rating = parseFloat(jQuery(cell).text()).toFixed(2);
    jQuery(cell).html(`<div><div class="rating">${rating}</div></div>`);
}
updateRating($(".divResult tr.Rating"));

And that’s now good enough.

I also notice that by removing the need for that findRating function to search all around the table, that it completely solves the error that you have been having too!


As a brief refresher, here is the original code that is now confirmed to have been causing the problem.

jQuery('.divResult table tbody tr td').each(function ($) {
    if (jQuery(this).text() == 'Rating') jQuery(this).nextAll("td").each(function ($) {
        jQuery(this).html('<div style="display:inline-block; width:100%;"><div class="rating" style="background-color:#558000">' +  parseFloat(jQuery(this).text()).toFixed(2) + '</div></div>');                                  
    });    
});        

A minimal improvement to fix the problem is to use tr.Rating, letting us remove one level of the function loop.

jQuery('.divResult tr.Rating').each(function ($) {
    jQuery(this).html('<div style="display:inline-block; width:100%;"><div class="rating" style="background-color:#558000">' +  parseFloat(jQuery(this).text()).toFixed(2) + '</div></div>');                                  
});        

My linted code takes that further to be:

function updateRating(cell) {
    var rating = parseFloat(jQuery(cell).text()).toFixed(2);
    jQuery(cell).html(`<div><div class="rating">${rating}</div></div>`);
}
updateRating($(".divResult tr.Rating"));

/me whines - I haven’t even fully completed doing a basic linting of the code yet!

Now that your problem is solved without needing timeouts or attaching on to message passing, are you interested in further work that gets done with the code?

Hi Paul,
Amazing work and, once again, extremely grateful to you for taking this much of time out and having this much of patience for a newbie like me. :smiley:

I’m going to rework my entire code basis your suggestions and will have a go at it. Hopefully this should resolve my issue.

Would it also be possible for you to share the revised html? Only if possible. Just so that i dont miss out on anything or mess it up somewhere.

Thanks again, Paul. :cowboy_hat_face:

1 Like

Thank you. I look forward to taking things further.

On checking things over the morning later, I find that I didn’t fix things, instead I broke things so that the problem didn’t end up occurring. That says a lot about the trouble with late-night coding.

So, instead we end up with the following code, and the error that you are getting persists.

function updateRating(cell) {
    var rating = parseFloat(jQuery(cell).text()).toFixed(2);
    jQuery(cell).html(`<div><div class="rating">${rating}</div></div>`);
}
function updateRatings(ratingCell) {
    if (jQuery(ratingCell).text() === "Rating") {
        jQuery(ratingCell).nextAll("td").toArray().forEach(updateRating);
    }
}
$(".divResult .Rating").toArray().forEach(updateRatings);

Handling arrays

Instead of using querySelector I have reverted back to the $ jQuery object for querying elements. Using querySelector hides the problem that jQuery has with arrays. As a result, instead of getting an array from querySelector

document.querySelectorAll(".divResult .Rating").forEach(updateRatings);

I want to use $ instead and use toArray, so that it’s very clear that we are turning our nose up at jQuery’s way of doing arrays, and preferring to use the built-in array-handling methods instead.

$(".divResult .Rating").toArray().forEach(updateRatings);

The linting continues

There are three move situations of long lines that the linter is not happy with. two of them in ranking, and one of them being the weather link.

Linting the ranking code

Here is the ranking code as it currently stands:

    jQuery(".divResult table tbody tr").each(function (ignore, tr) {
        var $td = $(tr).children();
        if ($td.eq(0).text() === "Ranking") {
            ranks.forEach(function (rank, i) {
                if (rank === 1) {
                    $td.eq(i+1).empty().append($(`<div style="display:inline-block; width:100%;"><div class="ranking1">#` + rank + "</div></div>"));
                }
                if (rank !== 1) {
                    $td.eq(i+1).empty().append($(`<div style="display:inline-block; width:100%;"><div class="ranking">#` + rank + "</div></div>"));
                }
            });
        }
    });

There are several things to deal with there but first, our attention must be with the line length.

The first thing to notice is that the styles should not be there at all. We dealt with that on the ratings code earlier, so we can now also use that with these rankings too.

.ranking, .rating {
    background-color:#558000;
}

The styles can now be removed from that part of the code:

                if (rank === 1) {
                    $td.eq(i+1).empty().append($(`<div><div class="ranking1">#` + rank + "</div></div>"));
                }
                if (rank !== 1) {
                    $td.eq(i+1).empty().append($(`<div><div class="ranking">#` + rank + "</div></div>"));
                }

That kind of if structure is really painful to see. But I’ll come back to that after getting the too long length dealt with.

A useful technique to deal with length and also make the code easier to understand, is to move the boy of if-statements out to a separate function. We can easily do that here, and with careful use of function properties, we can use the same function for both if statements.

    function updateRanking(cell, rank, className) {
        cell.empty().append($(`<div><div ${className}>#${rank}</div></div>`));
    }
...
                if (rank === 1) {
                    updateRanking($td.eq(i+1), rank, "ranking1");
                }
                if (rank !== 1) {
                    updateRanking($td.eq(i+1), rank, "ranking");
                }

One way to improve this is to extract the ranking to a separate variable, so that we can separately deal with giving it a different name:

                var rankingClass = (rank === 1 ? "ranking1" : ranking);
                updateRanking($td.eq(i+1), rank, rankingClass);

I can understand why the first ranking is being given a different classname, but usually using a completely different classname isn’t the way to do it. Instead, it’s more usual for them all to have the same ranking class name and then to add a special `first’ classname to the first one. To not change things too much on you though I’ll leave it at ‘ranking1’ that’s also on there. That way, the first one will have two classes on it, “ranking ranking1” and the rest of them just have “ranking”

That way we can also improve the code, so that only two function parameters are needed.

    function updateRanking(cell, rank) {
        cell.empty().append($(`<div><div "ranking">#${rank}</div></div>`));
    }
...
                updateRanking($td.eq(i+1), rank);
                if (rank === 1) {
                    $td.eq(i+1).addClass("ranking1");
                }

Linting the weather link

This is the last section that the linter complains about for being too long:

    if (jQuery(td).text() === "Weather") {
        jQuery(td).nextAll("td").each(function (ignore, el) {
            jQuery(el).html(`<a class="weatherwidget-io" href="https://forecast7.com/en/` + jQuery(el).text() + `/" data-label_2="WEATHER" data-days="3" data-theme="original">WEATHER</a>`);
        });
    }

First I’ll deal with that ignore, by using toArray() and forEach() instead.

    if (jQuery(td).text() === "Weather") {
        jQuery(td).nextAll("td").toArray().forEach(function (el) {
            jQuery(el).html(`<a class="weatherwidget-io" href="https://forecast7.com/en/` + jQuery(el).text() + `/" data-label_2="WEATHER" data-days="3" data-theme="original">WEATHER</a>`);
        });
    }

We can easily deal with that by extracting out the text to a separate variable:

    if (jQuery(td).text() === "Weather") {
        jQuery(td).nextAll("td").toArray().forEach(function (el) {
            var linkHtml = `<a class="weatherwidget-io" href="https://forecast7.com/en/` + jQuery(el).text() + `/" data-label_2="WEATHER" data-days="3" data-theme="original">WEATHER</a>`;
            jQuery(el).html(linkHtml);
        });
    }

and then the linkHref variable can be split up to deal with the line length issue.

    if (jQuery(td).text() === "Weather") {
        jQuery(td).nextAll("td").toArray().forEach(function (el) {
            var linkHtml = `<a class="weatherwidget-io"`;
            linkHtml += ` href="https://forecast7.com/en/` + jQuery(el).text() + `/"`;
            linkHtml += ` data-label_2="WEATHER" data-days="3" data-theme="original"`;
            linkHtml += ` >WEATHER</a>`;
            jQuery(el).html(linkHtml);
        });
    }

But that is still too long for the linter to be happy about it. The data attributes need to be split up, but I still want to keep them meaningfully together. That can be done by moving the href to the start of the link, and having one of the data attributes come after the class.

    if (jQuery(td).text() === "Weather") {
        jQuery(td).nextAll("td").toArray().forEach(function (el) {
            var linkHtml = `<a href="https://forecast7.com/en/` + jQuery(el).text() + `/"`;
            linkHtml += ` class="weatherwidget-io" data-label_2="WEATHER"`;
            linkHtml += ` data-days="3" data-theme="original">WEATHER</a>`;
            jQuery(el).html(linkHtml);
        });
    }

Now even if we reduce jQuery to just $ and use a template variable reference, the href line will still be too long:

            var linkHtml = `<a href="https://forecast7.com/en/${$(el).text()}/"`;

We need to extract out the path to a separate variable, and from there we can make beneficial progress.

    if (jQuery(td).text() === "Weather") {
        jQuery(td).nextAll("td").toArray().forEach(function (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>`;
            jQuery(el).html(linkHtml);
        });
    }

That ends the issue with too-long lines, and the linter now shows me a whole new set of issue with the code, which I’ll take care of in the next post.

When those other linting issues occurred, I was reminded that I haven’t properly formatted the code. What happened is that after wrapping single-line if statements with braces, that helps JS Beautifier to properly indent the code. But I hadn’t updated the indenting.

After running the code through JSBeautifier again the code is now all properly indented, and I have a nice long list of 10 lines that are still too long.

Taking care of long lines

            jQuery("#selection").find(":selected").each(function(ignore, option) {

This one reminds me that I really need to go through the code removing that ugly patch of using ignore, and instead replace it with more appropriate toArray().forEach() instead. I’ll do that now.

            // jQuery("#selection").find(":selected").each(function(ignore, option) {
            jQuery("#selection").find(":selected").toArray().forEach(function(option) {

Now back to dealing with line length.

The jQuery is normally only used for the initial document ready wrapper. After that the $ symbol should be used instead.

jQuery(document).ready(function($) {

    jQuery(document).ready(function($) {

        var StatJSON = {

Hmm, that’s odd. Why does your code have multiple document ready wrappers? Removing those extra ones will definitely help with the long lines problem.

Also, renaming jQuery to $ everywhere inside of the document ready wrapper takes care of more long lines, leaving us only with 4 long lines.

Long line #1

The first long line is in the button submit handler.

        $(".divResult table th:not(:first)").toArray().forEach(function(header) {
            var columnHead = $(header).text().split(" ").join("");
            $(header).addClass($(header).text().split(" ").join(""));
        });

Here we can extract the query to a named variable:

        var headings = $(".divResult table th:not(:first)");
        headings.toArray().forEach(function(header) {
            var columnHead = $(header).text().split(" ").join("");
            $(header).addClass($(header).text().split(" ").join(""));
        });

Long lines 2 through to 4

These are in the weather link code that we dealt with earlier:

            if ($(td).text() === "Weather") {
                $(td).nextAll("td").toArray().forEach(function(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);
                });
            }

Here, while it is possible to shorten the linkHtml variable, a more appropriate way is to extract the looping function instead.

        function addWeatherLink(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(addWeatherLink);
            }
            __weatherwidget_init();
        });

That is all of the too-long lines dealt with, and we can now move on to the rest of the linting.

Linting the rest of the code

Many of the linting issues are about using anonymous functions. We really need to name those. Something that I’ve been noticing about the code is that while it says what it does, the code doesn’t say why it does things. The first is necessary for the computer, but the second is really important for the human programmer.

That issue is remedied a lot by using good names for things.

One of the hardest parts of programming is finding good names for things. I can’t promise to find good names, but I’ll at least attempt to use meaningful names and seek to improve them where possible.

Naming the functions

Naming document ready function

For the document ready function it seems appropriate to name that one weatherTable, because it ranks the weather. I can’t help but find some measure of enjoyment too that rankWeather also works in terms of “bad weather” as well.

jQuery(document).ready(function rankWeather($) {

Naming a heading function

Here is how the code is before naming the function:

            $.each(data, function(i) {
                //getting heading at that key
                html += "<th>" + StatJSON[data[i]].ColHeading + "</th>";
            });

And afterwards the comment is found to be misleading.

            $.each(data, function addColumnHeading(i) {
                //getting heading at that key
                html += "<th>" + StatJSON[data[i]].ColHeading + "</th>";
            });

The heading isn’t being obtained. Instead it is being added as a table element. Because the function name provides enough of an understanding here now, the misleading comment should either be corrected, or removed. When comments are corrected usually that helps me to understand that the code is weak in that area, so I try to improve the code so that the comment isn’t needed. In this case the comment can be removed as it doesn’t add anything to what we know already.

            $.each(data, function addColumnHeading(i) {
                html += "<th>" + StatJSON[data[i]].ColHeading + "</th>";
            });

Naming add table rows

This next part of the code needs naming too:

            $.each(StatJSON[data[0]], function(k, v) {
                html += "<tr>";

Here we are adding table rows, so addRow is a good function name.

The linter also tells me that v isn’t used, so that gets removed.

            $.each(StatJSON[data[0]], function addRow(k) {
                html += "<tr>";

I also tell myself that single letter variable names are almost completely useless. Here, k is the property names of each option in your StatJSON array. That is what they are, but we should give it a name for what they mean.

Here, they are the row headings, so that’s what we should name them.

            $.each(StatJSON[data[0]], function addRow(rowTitle) {
                html += "<tr>";
                if (rowTitle !== "ColHeading") {
                    html += "<td>" + rowTitle + "</td>";
                }

Naming add data function

The next function that needs naming is:

                $.each(data, function(k2, v2) {
                    if (rowTitle !== "ColHeading") {
                        html += "<td>" + StatJSON[data[k2]][rowTitle] + "</td>";
                    }
                });

Here it’s adding data from StatJSON, so calling it addData is good enough, especially in the context with the addRow function too.

                $.each(data, function addData(k2, v2) {
                    if (rowTitle !== "ColHeading") {
                        html += "<td>" + StatJSON[data[k2]][rowTitle] + "</td>";
                    }
                });

The linter tells me that v2 isn’t used, and I tell myself that k2 needs to be renamed to something better. While k2 is an option name such as “Option2”, that’s only what it is and doesn’t tell us why it’s important to us. Each of those options refers to info about a city, so cityInfo seems to be a good name to use there.

                $.each(data, function addData(cityInfo) {
                    if (rowTitle !== "ColHeading") {
                        html += "<td>" + StatJSON[data[cityInfo]][rowTitle] + "</td>";
                    }
                });

We now get told that the line is too long, so I’ll extract out a named variable from there.

                $.each(data, function addData(cityInfo) {
                    if (rowTitle !== "ColHeading") {
                        var cityData = StatJSON[data[cityInfo]][rowTitle];
                        html += "<td>" + cityData + "</td>";
                    }
                });

Naming button submit code

Here’s the next piece that the linter says needs a function name:

    $("#btnSubmit").click(function() {
        var data = [];
        ...
    });

That function is an event handler for submitting the cities, so calling it citiesSubmitHandler is a good and standard way of naming it.

    $("#btnSubmit").click(function citiesSubmitHandler() {
        var data = [];

Now with event handlers, it’s best if they do very little work at all themself, and pass on execution to other functions. I’m tempted to add a TODO comment to the code, but the linter doesn’t allow anything else to be done until those TODO’s are taken out.

So, I guess that I had better move out the code now before it gets forgotten about.

And that is going to be the topic of my next post.

1 Like

Thanks Paul!! Amazing level of detailing in your explanations!! It has actually explained a lot of things to me and anyone who is following this thread. It has also solved a lot of small doubts I used to have but never knew who to ask. :cowboy_hat_face:

Once again, thanks, and looking forward to your solution. :smiley:

2 Likes

Where we left off was in giving functions appropriate names, helping us to make the code easier to understand in the process, where extracting out functions from the submit handler is the next piece of the puzzle.

Extract functions from event handler

When doing this I find that it’s easier to chunk the code into separate functions by working up from the bottom. When working your way down from the top it’s more difficult to tell where one section of code becomes significantly different from the next. By contrast when working up from the bottom, it’s easier to tell where to find the seams.

In this case we find that the bottom section is about adding weather links, so we move that into a separate function and move that function out of the handler.

    function addWeatherLinks() {
        function addWeatherLink(el) {
            ...
        }
        ...
    }
    $("#btnSubmit").click(function citiesSubmitHandler() {
        ...
        addWeatherLinks();
    });

Chunking the code into separate functions also means that before I can give the function a suitable name, I need to try and understand what the code does, which means examining it more closely than I otherwise might have.

I won’t bore you with the details of the other functions, but here’s what the event handler ends up becoming:

    $("#btnSubmit").click(function citiesSubmitHandler() {
        updateWeatherTable();
        cleanupTextAndClasses();
        updateRatings();
        updateRankings();
        addWeatherWidget();
    });

Naming the get cities function

This function in the updateWeatherTable function needs to be named.

    function updateWeatherTable() {
        var data = [];
        $("#selection").find(":selected").toArray().forEach(function (option) {
            var this_input = $(option);
            if (this_input.is(":selected")) {
                data.push(this_input.val());
            }
        });
        $("#divResult").empty().append(printTable(data));
    }

This code gets the cities that you selected, so calling the function getSelectedCity seems to work. That results in the line being too long, so we can extract the getSelectedCity function from there.

        function getSelectedCity(option) {
            var this_input = $(option);
            if (this_input.is(":selected")) {
                data.push(this_input.val());
            }
        }
...
        $("#selection").find(":selected").toArray().forEach(getSelectedCity);

There are some issues with this code though. The main problem is that we have an array that forEach is updating. Instead of that, we can usefully use a map method. Normally I would just remove most of the code and write it over from scratch, but it might be useful for you to see the process of how it’s converted.

The separate this_input variable is left over from previously using the this keyword. That keyword is so generic that you tend to be forced to rename it just to understand what is happening. That’s not needed there now, because we have the option variable instead.

        function getSelectedCity(option) {
            if ($(option).is(":selected")) {
                data.push($(option).val());
            }
        }
        var data = [];
        $("#selection").find(":selected").toArray().forEach(getSelectedCity);

The $(option) is duplicated and doesn’t need to be. Instead of using $(option).val() it’s just as good, if not better, to just use option.value instead.

        function getSelectedCity(option) {
            if ($(option).is(":selected")) {
                data.push(option.value);
            }
        }        
        var data = [];
        $("#selection").find(":selected").toArray().forEach(getSelectedCity);

The selected is not needed in the getSelectedCities function. That is already being done outside of the function. When we remove selected from the function, we should also rename the function at the same time too.

        function getCity(option) {
            data.push(option.value);
        }        
        var data = [];
        $("#selection").find(":selected").toArray().forEach(getCity);

We now have a very simple function that just adds a new item to the array. The map method is one that updated forEach, buy giving you an array of returned values. We can use map instead here to simplify things.’

        function getCity(option) {
            return option.value;
        }
        var data = $("#selection").find(":selected").toArray().map(getCity);

Because the getCity function is so simple, it doesn’t really deserve to have three lines of code dedicated to it. This here is what arrow-notation was designed for.

        var getCity = (option) => option.value;
        var data = $("#selection").find(":selected").toArray().map(getCity);

That all results in the updateWeatherTable function being updated to be the following:

    function updateWeatherTable() {
        var getCity = (option) => option.value;
        var data = $("#selection").find(":selected").toArray().map(getCity);
        $("#divResult").empty().append(printTable(data));
    }

That’s a pretty nice improvement on what we had before. It has a pretty nice structure too, where each variable is defined, and is used on the very next line.

I’ll leave things there for the moment though as it’s past 1am. More will be forthcoming as I carry on working through the JSLint linting issues when I return.

1 Like