Improving code by changing demands

Recently I’ve been noticing that tutorials supply code in it’s fully formed nature, leaving people confused about if they are expected to directly write code that works in the same way.
But, code doesn’t come from us as fully formed working code. It usually goes through a process of modification to make things work better.

I plan to use a series of posts here to demonstrate that code doesn’t normally start off great. Instead, it’s improved over time as it’s forced to work under different situations.

An initial attempt

A niave attempt to start off with is a count function that we plan to expand on:

<script>
function counter() {
    console.log(this.value.length);
}
</script>
<p><textarea oninput="counter()"></textarea></p>

That doesn’t work though, because the inline oninput method doesn’t let the function know how it was called.

  • Problem: Inline event handlers don’t communicate well with functions.
  • Solution: Pass information to the function about what to work with.

Pass element reference to the function

Passing the this keyword to the count function, gives a reference to the element letting us easily work with it.

function counter(textarea) {
    console.log(textarea.value.length);
}
<p><textarea oninput="counter(this)"></textarea></p>

And it works. The browser console shows the updated character count when you edit the text in the textarea section.

But, we want that number to show on the screen beside the textarea.

We can add a span element to show the character count, and we’ll start by showing a default character limit of 50 characters:

<p>
    <textarea oninput="counter(this)"></textarea>
    <span id="counter">0</span>/50
</p>

Web browsers automatically let you access identified elements which we’ll use for now, but there are problems with that too. We’ll get those problems dealt with later.

We can now update the count function so that it updates the span element:

function counter(textarea) {
    counter.innerHTML = textarea.value.length;
}

But that doesn’t quite work, because there’s a clash between the function name and the id attribute on the page.

  • Problem: Element identifiers get clobbered by other variables.
  • Solution: Use querySelector to refer to HTML elements.

Access element by reference

We can rename the function to countChars to resolve the conflict, but a more reliable solution is to use querySelector to gain a reference to the element. We’ll want to rename the function name too, to prevent the function name and the variable, so that they don’t end up clashing with each other too.

<script>
function countChars(textarea) {
    const counter = document.querySelector("#counter");
    counter.innerHTML = textarea.value.length;
}
</script>
<p>
    <textarea oninput="countChars(this)"></textarea>
    <span id="counter">0</span>/50
</p>

And that works, case closed.

Or it would be, but there are problems:

  • countChars function assumes only one textarea
  • inline attribute event handlers don’t belong in the HTML code
  • The span counter is meaningless when scripting doesn’t work

In the following posts I’m going to change the situations in which the code must work, to force beneficial changes to the code.

6 Likes

How does the code need to change when there are several textarea sections for the counter?

Dealing with several textareas

It’s not possible to have the same identifier on each counter, as they conflict with each other:

<!-- Non-working code because of identical identifiers -->
<p><textarea oninput="countChars(this)"></textarea>
<span id="counter">0</span>/50</p>
<p><textarea oninput="countChars(this)"></textarea>
<span id="counter">0</span>/50</p>
<p><textarea oninput="countChars(this)"></textarea>
<span id="counter">0</span>/50</p>
...

ID attributes must be unique. We could use a different identifier on each counter, but then we would also need to tell countChars about that different identifiers.

<!-- Inappropriate code because of busy programming ending up in HTML -->
<p><textarea oninput="countChars(this, 'counter1')"></textarea>
<span id="counter1">0</span>/50</p>
<p><textarea oninput="countChars(this, 'counter2')"></textarea>
<span id="counter2">0</span>/50</p>
<p><textarea oninput="countChars(this, 'counter3')"></textarea>
<span id="counter3">0</span>/50</p>
...

Another sign of badness is when we need to sequentially number things. That’s often a very good sign that there are better ways to be found.

  • Problem: Don’t want to individually number different elements.
  • Solution: Use grouping and relative positions to avoid named elements.

Adding counters to multiple textareas

The countChars function currently makes an assumption about the counter. It assumes that the counter variable resolves to only one HTML element. That’s no longer the case.

Instead of naming the counter, as it is directly after the textarea element we can use a classname instead, or as we already have easy access to the textarea, we can retrieve the element immediately after the textarea.

<script>
    function countChars(textarea) {
        // const counter = document.querySelector("#counter");
        const counter = textarea.nextElementSibling;
        counter.innerHTML = textarea.value.length;
    }
</script>
<p>
    <textarea oninput="countChars(this)"></textarea>
    <span>0</span>/50
</p>
<p>
    <textarea oninput="countChars(this)"></textarea>
    <span>0</span>/50
</p>
<p>
    <textarea oninput="countChars(this)"></textarea>
    <span>0</span>/50
</p>
...

The span section containing the character count doesn’t necessarily need to be named with a classname to work properly.

The HTML code for each counter is becoming rather busy though. There are many ways to deal with that, and next time I’ll introduce a different situation to help drive the code to a better state, so that it can more easily handle things.

2 Likes

With the HTML code getting very busy with oninput handlers and span sections for counters, what technique are we going to use to force beneficial change?

Competition!

When there are several other text-counter scripts out there, including ours, which one are people are going to use? They’re more likely to use the one that’s easier to use.

Are people more likely to use a script that means changing their textarea code from <textarea></textarea> to:

<textarea oninput="countChars(this)"></textarea>
<span>0</span>/50

and make the above change to each and every textarea that they have on the page?

Or, are people more likely to instead use a text-counter where they don’t need to change anything at all?

<textarea></textarea>

Whenever we add a textarea counter to a web page, it’s best if as few things as possible need to change. The text-counter script that’s the easiest to use tends to get used the most, and that applies for our own code that we use too. Having our own textarea counter script add that information to the page ends up being a direct benefit to ourself.

  • Problem: Demanding changes are required to HTML code for your script to be used.
  • Solution: Use scripting to add HTML that only benefits scripts.

Scripting always works, right?

Another benefit of the scripting adding HTML code that’s only relevant to scripting, is that it prevents meaningless on-screen information when scripting is unavailable. It’s not always gnaranteed that scripting is available for a page, so it’s best that a page behaves itself when scripting doesn’t work. Afterall, Everyone has JavaScript, right? Not always.

It’s a best-practice technique for HTML pages to avoid showing content that only makes sense when scripting occurs.

Next Steps

We now have two tasks on the agenda for improving the code.

  • Use scripting to add the span elements into the HTML code.
  • Use scripting to add an event handler to the textarea instead of the existing inline attribute events.

A few problems also occur while completing those tasks, which I’ll go over in my next post.

2 Likes

Use scripting to add the text counters

When someone’s working on the HTML code, we don’t want them to worry about what the scripting code does. They shouldn’t need to know or worry about the scripting when working on the HTML.

From the HTML code we remove the counters, and have the scripting code add them instead. I’ve commented them out below to indicate that it’s been removed, but you can delete them entirely from your code.

<!--<p>
    <textarea oninput="countChars(this)"></textarea>
    <span>0</span>
</p>
<p>
    <textarea oninput="countChars(this)"></textarea>
    <span>0</span>
</p>
<p>
    <textarea oninput="countChars(this)"></textarea>
    <span>0</span>
</p>-->
<p><textarea oninput="countChars(this)"></textarea></p>
<p><textarea oninput="countChars(this)"></textarea></p>
<p><textarea oninput="countChars(this)"></textarea></p>

The character counter cannot be added from the countChars function, because we need the counter to be seen before we interact with the textarea. We also cannot access the textarea from script that runs before it.

That means that we need to either use an onload event to add the counter, or use some separate code after the textarea to add the counter.

Back in the olden days we would have used an onload event to add the counter because scripts only worked from the head section of HTML and not the body. That’s a problem we now don’t have to worry about, and much better results are now achieved by placing the script at the end of the body, after all of the HTML content instead.

One of the counters can be added by placing this code in a separate script section below the textareas:

<script>
var textarea = document.getElementsByTagName("textarea")[0];
textarea.parentNode.innerHTML += "<span>0</span>/50";
</script>

Right now it’s only the first textarea that gets the counter added to it. We need it to be added to all of the textareas.

As we have multiple counters, we need to loop through each of the textarea elements that are found and add a counter to each one of them.

Add counter to all textareas

We can get all of the textareas using document.getElementsByTagName("textarea") and loop through each of them usign a for loop.

// the following code works, but can be improved
var textareas = document.getElementsByTagName("textarea");
for (var i = 0; i < textareas.length; i += 1) {
    textareas[i].parentNode.innerHTML += "<span>0</span>/50";
}

But surely, there’s a better way to do that today? The index variable of a for loop is an unnecessary complication that’s not needed these days.

Other code structures exist such as for…of to access the elements, but that doesn’t work in Internet Explorer.

// the following for...of isn't supported by Internet Explorer
var textareas = document.getElementsByTagName("textarea");
for (var textarea of textareas) {
    textareas[i].parentNode.innerHTML += "<span>0</span>/50";
}
  • Problem: For loops, for…of and for…in become problematic.
  • Solution: Prefer forEach iteration over for, for…in or for…of loops.

Arrays do have the Array.from method to create an array from other arraylike objects, but that’s not supported by Internet Explorer.

// the following Array.from isn't supported by Internet Explorer
var textareas = document.getElementsByTagName("textarea");
Array.from(textareas).forEach(function (textarea) {
    textarea.parentNode.innerHTML += "<span>0</span>/50";
});

What about getting a nodeList from querySelectorAll, and using the nodeList forEach method?

// the following textareas.forEach isn't supported by Internet Explorer
var textareas = document.querySelectorAll("textarea");
textareas.forEach(function (textarea) {
    textarea.parentNode.innerHTML += "<span>0</span>/50";
});

Nope, that’s not supported by Internet Explorer either.

If we didn’t have to care about Internet Explorer this would be much easier, but IE is still being used by 7% of browser visitors.

A technique that does with with IE is to use Array.prototype.forEach.call, but that’s quite a handful to use, and feels too much like a hack to get things working.

// the following works, but Array.prototype.forEach.call is complicated
var textareas = document.querySelectorAll("textarea");
Array.prototype.forEach.call(textareas, function (textarea) {
    textarea.parentNode.innerHTML += "<span>0</span>/50";
});

Because of issues such as the above, polyfills can be used instead. A polyfill is some code that helps to fill in missing features from a browser. In this case, it’s nodeList support for the forEach method.

  • Problem: Is a handy feature not supported by some browsers?
  • Solution: Polyfills help you to write easier-to-understand code.

Here’s the polyfill that lets browsers use forEach on list of nodes:

if (window.NodeList && !NodeList.prototype.forEach) {
    NodeList.prototype.forEach = Array.prototype.forEach;
}

With that polyfill in place, we can now use the much easier forEach method with textareas.forEach instead.

var textareas = document.querySelectorAll("textarea");
textareas.forEach(function (textarea) {
    textarea.parentNode.innerHTML += "<span>0</span>/50";
});

Instead of using getElementsByTagName which gives us a live HTMLCollection object on which we’d want to use a for loop, or for…of, we can use querySelectorAll instead, along with forEach method to iterate over each element.

Using querySelectorAll also acts as protection against future change as getElementsByTagName only works with elements, whereas querySelectorAll also works with unique identifiers and class names.

Here’s the updated scripting code that uses querySelectorAll and the forEach method.

if (window.NodeList && !NodeList.prototype.forEach) {
    NodeList.prototype.forEach = Array.prototype.forEach;
}
var textareas = document.querySelectorAll("textarea");
textareas.forEach(function (textarea) {
    textarea.parentNode.innerHTML += "<span>0</span>/50";
});

That fixes some of the problems, but has added other ones to the list.

  • countChars function assumes only one textarea
  • the span counter is meaningless when scripting doesn’t work
  • inline attribute event handler doesn’t belong in HTML code
  • counter assumes a limit of 50 characters
  • multiple sections of scripting code, some above and some below
  • relies on the parentNode having nothing after textarea
  • innerHTML means using HTML code inside of JavaScript

While working with the character count, not all textareas require the same 50 char limit. We’ll do something about that in my next post.

2 Likes

As this task had us working with the counter display, I’m reminded that we are making assumptions about the 50 character limit, which is easy to improve on now.

Allowing multiple types of character limits

There are many ways to configuring the number of characters allowed for a textarea, such as by initializing them from JavaScript, or by providing the information directly on the textarea element itself.

However, the textarea already has a maxlength attribute, so we can leverage that capability. If there is no maxlength specified, we can default to 50 chars, and later on investigate how to easily configure that default value.

    <p><textarea maxlength="15" oninput="countChars(this)"></textarea></p>
    <p><textarea oninput="countChars(this)"></textarea></p>
    <p><textarea maxlength="30" oninput="countChars(this)"></textarea></p>

The maxlength attribute is supported in a wide range of web browsers, and our script serves to help inform people about that limit before they reach it.

I notice though that with the maxlength is specified, that the textarea refuses to accept more text, but with a default max of 50 being shown that more text is allowed to be entered. I think that preventing more text might be forcing things too far, but we can certainly make the counter red to give a visual cue that you’ve blown past the limit. That’s something we can do later.

We can easily get the max length from within the forEach method:

textareas.forEach(function (textarea) {
    var maxLen = textarea.getAttribute("maxlength") || 50;
    textarea.parentNode.innerHTML += "<span>0</span>/" + maxLen;
});

Move the scripts together

Having the code achieve our task all in one file is preferable, as that makes it easier for people to add this script to their own web pages. Scattering the scripting code all over the web page is not a good practice, as it make it more difficult to work with the scripting code.

A long time ago we used to load scripting files from the head section of the code, but that made it difficult to work with elements on the page. These days we load scripts from the end of the body, which helps to keep everything together and easily accessible.

  • Problem: Scripts are difficult when scattered all over the place.
  • Solution: Load script file from the end of the HTML body.

The HTML code ends up being:

<!DOCTYPE html>
<head>
    <title>Textarea counter</title>
</head>
<body>
    <p><textarea maxlength="15" oninput="countChars(this)"></textarea></p>
    <p><textarea oninput="countChars(this)"></textarea></p>
    <p><textarea maxlength="30" oninput="countChars(this)"></textarea></p>
    <script src="textarea-count.js"></script>
<body>
</html>

With script.js containing our scripting code:

For now though as we’re using jsfiddle to experiment with the code, we can leave the scripting code in the JavaScript section, and configure it to load the script at the bottom of the body. https://jsfiddle.net/pmw57/1sx3e5jy/

If another couple of polyfills end up being added to our code, that’s a good time to consider moving them out to a separate polyfill.js file too.

Let’s now take another look at that list of problems that we had.

  • script should be at the end of the document
  • countChars function assumes only one textarea
  • the span counter is meaningless when scripting doesn’t work
  • counter assumes a limit of 50 characters
  • multiple textareas results in multiple problems
  • scripting code is split, some above and some below
  • only supports one text area
  • inline attribute event handler should be in the scripting code instead
  • innerHTML means using HTML code inside of JavaScript
  • set default textarea character limit
  • change counter color to red when limit is reached

The code as it currently stands is found at https://jsfiddle.net/pmw57/1sx3e5jy/ - there’s a lot of improvement that’s been made to the code, but we’re only just starting.

The next improvement is to carry on with the simplification and remove the inline event handers, which I’ll cover in my next post.

2 Likes

Replacing inline event handlers

To replace the inline event handlers, we’ll use addEventListener to add a handler function for the input events.

function textareaInputHandler(evt) {
  var textarea = evt.target;
  countChars(textarea);
}

As we plan to use addEventHandler instead, that gives an event object to the handler function. We can prepare for that change by replacing the inline oninput handler with one thatuses the event object.

<p><textarea maxlength="15" oninput="textareaInputHandler(event)"></textarea></p>
<p><textarea oninput="textareaInputHandler(event)"></textarea></p>
<p><textarea maxlength="30" oninput="textareaInputHandler(event)"></textarea></p>

We can now remove the inline oninput event handler and use scripting instead to add the event handler.

<p><textarea maxlength="15"></textarea></p>
<p><textarea></textarea></p>
<p><textarea maxlength="30"></textarea></p>
textareas = document.querySelectorAll("textarea");
textareas.forEach(function addTextareaHandler(textarea) {
  textarea.addEventListener("input", textareaInputHandler);
});

The above querySelectorAll command needs to happen again after the counters are added to the page to get things working. Investigating why that’s needed, and how to fix things, is what we investigate next.

Troubleshooting the real cause of a problem

We can remove the textarea assignment to start investigating the cause of the problem. I’ve commented out the appropriate line in the excerpt below.

...
var textareas = document.querySelectorAll("textarea");
textareas.forEach(function (textarea) {
    var maxLen = textarea.getAttribute("maxlength") || 50;
    textarea.parentNode.innerHTML += "<span>0</span>/" + maxLen;
});
function textareaInputHandler(evt) {
  var textarea = evt.target;
  countChars(textarea);
}
// textareas = document.querySelectorAll("textarea");
textareas.forEach(function addTextareaHandler(textarea) {
  textarea.addEventListener("input", textareaInputHandler);
});

Why does the addEventListener statement no longer work? When I set a breakpoint in the forEach function, I find that the event handler that the addEventListener section adds, no longer exists on the textareas that we have on the page.

The cause of the problem is the innerHTML code. Why? When the innerHTML comes before addEventListener, the textareas list of elements no longer exist because the innerHTML creates new textareas. If we move the innerHTML code so that it comes after the addEventListener, it destroys the textarea with the event and replaces it with a different textarea element.

We can fix that problem by getting a new set of textareas from the page. That’s not a good solution though, as it’s a duplication of code just because of using innerHTML. Using innerHTML there has made our code fragile to change. We don’t want our code to be fragile to change, which is why we get rid of innerHTML. We can instead use appendChild to add a counter element instead.

Replacing innerHTML

Here’s the section of code that uses innerHTML:

var textareas = document.querySelectorAll("textarea");
textareas.forEach(function (textarea) {
    var maxLen = textarea.getAttribute("maxlength") || 50;
    textarea.parentNode.innerHTML += "<span>0</span>/" + maxLen;
});

While investigating the code, I saw in the Call Stack that the function just says anonymous. It’s long past time to name functions so that the Call Stack shows meaningful information.

var textareas = document.querySelectorAll("textarea");
// textareas.forEach(function (textarea) {
textareas.forEach(function addCounter(textarea) {
    var maxLen = textarea.getAttribute("maxlength") || 50;
    textarea.parentNode.innerHTML += "<span>0</span>/" + maxLen;
});

To replace innerHTML we just need to create a span element and give it a first character of 0, onto which we can add the maxlen and append it to the textarea.

function createCounter(maxLen) {
    var counter = document.createDocumentFragment();
    var span = document.createElement("span");
    span.appendChild(document.createTextNode("0"));
    counter.appendChild(span);
    counter.appendChild(document.createTextNode("/" + maxLen));
    return counter;
}
textareas.forEach(function addCounter(textarea) {
    var maxLen = textarea.getAttribute("maxlength") || 50;
    // textarea.parentNode.innerHTML += "<span>0</span>/" + maxLen;
    var counter = createCounter(maxLen);
    textarea.parentNode.appendChild(counter);
});

Now that the annoying innerHTML is gone, we can easily add event handlers to the textarea elements.

Summary

All together, the full scripting code now looks like this:

function countChars(textarea) {
    const counter = textarea.nextElementSibling;
    counter.innerHTML = textarea.value.length;
}
function textareaInputHandler(evt) {
    const textarea = evt.target;
    countChars(textarea);
}

var textareas = document.querySelectorAll("textarea");
textareas.forEach(function addCounter(textarea) {
    var maxLen = textarea.getAttribute("maxlength") || 50;
    var counter = document.createDocumentFragment();
    var span = document.createElement("span");
    span.appendChild(document.createTextNode("0"));
    counter.appendChild(span);
    counter.appendChild(document.createTextNode("/" + maxLen));
    textarea.parentNode.appendChild(counter);
});
textareas.forEach(function addTextareaEvents(textarea) {
    textarea.addEventListener("input", textareaInputHandler);
});

The updated code is at https://jsfiddle.net/pmw57/1sx3e5jy/2/ and the following remaining problems have all been dealt with, leaving only a few remaining issues.

  • inline attribute event handler should be in the scripting code instead
  • innerHTML means using HTML code inside of JavaScript
  • set default textarea character limit
  • change counter color to red when limit is reached

We now have code that is a lot easier to use than it was before, and will take a look at those remaining issues next time.

1 Like

Set a default character limit

When it comes to setting a default character limit, we could have a function that changes the existing value to some other preferred default - but that’s an added complexity on top of being able to set a default value in the first place, which must be done first.

As each textarea might have a maxlength attribute, we’ll need to loop through each textarea and use either that maxlength, or the default value to update the counter.

We can move the code that adds the counter and max length, to a separate updateCounter function:

function updateCounter(textarea, defaultLen) {
    var maxLen = textarea.getAttribute("maxlength") || defaultLen;
    var span = textarea.nextElementSibling;
    counter.innerHTML = "<span>" + textarea.innerHTML.length + "</span>" +
        "/" + maxLen;
}

var defaultLen = 50;
var textareas = document.querySelectorAll("textarea");
textareas.forEach(function addCounter(textarea) {
    // var maxLen = textarea.getAttribute("maxlength") || 50;
    // var counter = document.createDocumentFragment();
    var span = document.createElement("span");
    // span.appendChild(document.createTextNode("0"));
    // counter.appendChild(span);
    // counter.appendChild(document.createTextNode("/" + maxLen));
    // textarea.parentNode.appendChild(counter);
    textarea.parentNode.appendChild(span);
    updateCounter(textarea, defaultLen);
});

That only works the first time though, but that’s enough for now.

Use init function to specify the default

We can move that code into an init function, letting us specify a preferred default value.

function initCounters(defaultLen) {
    // var defaultLen = 50;
    defaultLen = Number(defaultLen) || 50;
    var textareas = document.querySelectorAll("textarea");
    textareas.forEach(function addCounter(textarea) {
        var span = document.createElement("span");
        textarea.parentNode.appendChild(span);
        updateCounter(textarea, defaultLen);
    });
    textareas.forEach(function addTextareaEvents(textarea) {
        textarea.addEventListener("input", textareaInputHandler);
    });
}

That way, nothing happens when the code first loads, and you can get it started using initCounters, either with no value for the default of 50 chars, or you can specify your own default value too.

initCounters(60);

That works with the textareas that we currently have, which are all on different lines. But our code fails to work as soon as we have something after a textarea element. We’ll deal with next time.

2 Likes