Improving code by changing demands

#1

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
#2

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.

1 Like
#3

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.

1 Like
#4

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.

1 Like
#5

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.

1 Like