Lesson Monday

We now have a very basic application where our business and user interface logic are completely separate. Our business logic is tested with plain English tests. Our user interface doesn't include any business logic. Instead, whenever it needs to interact with business logic, it calls one of the functions in our business logic.

However, it's not always easy to tell if logic should be UI or business logic. So let's muddy the water a bit and look at an example where separation of logic may not be entirely clear. We're going to add another function to our application called boldMatches(). This function will return a string with bolded word matches. For instance, let's say we want to find matches of the word "world" in the string "Hello world! It truly is a wonderful day - and a wonderful world!"

Our string will look like this in the UI:

"Hello world! It truly is a wonderful day - and a wonderful world!"

First off, is this business logic or user interface logic?

The answer should be pretty obvious. It returns a formatted string to the user. Therefore it's user interface logic. But just because it's user interface logic doesn't mean we should just throw the new code into the $(document).ready() section of our code. We can still extract user interface logic into functions which will make our code much cleaner. Also, just because a function should be user interface logic doesn't mean it needs to directly alter the DOM. We can do better than that.

This is very important to reiterate because it would be really easy to just update our numberOfOccurrencesInText() to do this with the bare minimum amount of code. Here's what it might look like. We do not want to actually do this. It is just an example.

// Do not do this. It works but it does not separate logic!

function numberOfOccurrencesInText(word, text) {
  if (text.trim().length === 0) {
    return 0;
  }
  let htmlString = "<p>";
  const wordArray = text.split(" ");
  let wordCount = 0;
  wordArray.forEach(function(element) {
    if (element.toLowerCase().includes(word.toLowerCase())) {
      wordCount++;
      htmlString = htmlString.concat("<b>" + element + "</b>");
    } else {
      htmlString = htmlString.concat(element);
    }
    htmlString = htmlString.concat(" ");
  });
  $("#bolded-passage").html(htmlString + "</p>");
  return wordCount;
}

In this example, we create an htmlString with an opening p tag. Then when we loop through the wordArray, we don't just count the number of occurrences of a specific word. We also bold that word if it's a match. Finally, when the loop ends, we use jQuery's html method to print the string to the DOM before we return the word count.

Again, it should be obvious why this is bad. This function is just supposed to do one thing - count the number of occurrences of a word in a text. Sure, it may seem convenient to use the loop and conditional we already have to construct a string for the DOM. And it may seem particularly tempting because we can potentially solve the problem with less code if we do this. But if we do so, we won't be able to reuse the function elsewhere. If the HTML changes, the function will break. It's also harder to read and reason about what this function does - and it's just plain ugly.

And no, you can't escape the problem by returning an HTML string with the word count like this: return [wordCount, htmlString];. While our function would no longer directly interact with the DOM or use the html method, it would still be doing formatting work intended for the user interface. This interferes with a very important programming design pattern: separation of concerns. That means each function should only focus on one thing and not know about anything else in the application. In the case of numberOfOccurrencesInText(), it should take a string, determine how many times a specific substring occurs in that string, and then return the count. It shouldn't do anything else.

So let's do this the right way. We'll create a function that handles the UI logic for bolding a passage. We can even use TDD to test it, though keep in mind that TDD is generally used for business logic, not UI logic. (There are other ways to test UI logic such as end to end tests.) You will not be expected to write tests for your UI logic on your independent project. However, we are going to walk through the process of using TDD for this function because it can still help us break down the problem into smaller parts and it will help us separate our code better.

Let's start with a test:

Describe: boldPassage()

Test: "It should return a non-matching word in a p tag."
Code:
const word = "hello";
const text = "yo";
boldPassage(word, text);
Expected Output: "<p>yo</p>"

As you can probably guess, we aren't going to use the html method in this function. That will make it too hard to test. Instead, the function will just return a formatted string. No need to interact with the DOM at all! We keep it very simple. Both parameters are one word and the strings don't match.

Now let's get the test passing:

js/scripts.js
function boldPassage(word, text) {
  return "<p>" + text + "</p>";
}

As we can see, if the word's not a match, it shouldn't be bolded. Therefore we should just return the text wrapped in p tags. The test will pass.

Onto the second test!

Test: "It should return a matching word in a b tag."
Code:
const word = "hello";
const text = "hello";
boldPassage(word, text);
Expected Output: "<p><b>hello</b></p>"

Now the one-word strings match, which means they should be bolded. Here's the updated code:

js/scripts.js
function boldPassage(word, text) {
  if (word === text) {
    return "<p><b>" + text + "</b></p>";
  } else
  return "<p>" + text + "</p>";
}

This will get our test passing and will still be compliant with our first test.

Now let's move onto the third test:

Test: "It should wrap words that match in `b` tags but not words that don't."
Code:
const word = "hello";
const text = "hello there";
boldPassage(word, text);
Expected Output: "<p><b>hello</b> there</p>"

Now let's update our code to get the test passing. We are ready for a loop now. The function will change considerably but the conditional we've already written will still play an instrumental part in our code.

js/scripts.js
function boldPassage(word, text) {
  let htmlString = "<p>";
  let textArray = text.split(" ");
  textArray.forEach(function(element) {
    if (word === element) {
      htmlString = htmlString.concat("<b>" + element + "</b>");
    } else {
      htmlString = htmlString.concat(element);
    }
    htmlString = htmlString.concat(" ");
  });
  return htmlString + "</p>";
}

There is still one more thing we need to fix to get this test passing, but first let's go over the changes we've made.

First, we create an htmlString that starts with an opening <p> tag. We will concatenate additional words to this string with our loop.

We take our split array and then loop through it. If the word and the element match, we should bold the word in the htmlString by bolding it. That means we do the following: htmlString = htmlString.concat("<b>" + element + "</b>");.

We use String.prototype.concat(), one of the most useful JavaScript string methods out there. Because this method does not change the receiver (it's not destructive), we can't just do htmlString.concat("<b>" + element + "</b>");. If we did, htmlString would never get updated because String.prototype.concat() will not change it. That's why we need to update htmlString like this instead: htmlString = htmlString.concat("<b>" + element + "</b>");. We are assigning a new value to htmlString with =.

If the word and the element don't match, we just concatenate the element without b tags.

Then, after we've concatenated the element, we add a space with htmlString = htmlString.concat(" "); regardless of whether the element was a match. This is because when we split our string based on spaces, we also removed those spaces. So now we need to add them back in.

Finally, we return the htmlString with a closing p tag added.

If we try this test out in the console, we will see that something is not quite right:

"<p><b>hello</b> there </p>"

Our method adds an extra space at the end. How can we fix it?

Well, this is a great opportunity to cover an important feature of Array.prototype.forEach() that we haven't discussed in detail yet. We can also pass in an index like this:

const string = "I like cats!";
string.split(" ").forEach(function(element, index) {
  console.log(element, index);
});

Try the above code in the console and the following will be logged:

I 0
like 1
cats! 2

So we can get both the element and the current position of the array. This is a very useful piece of information and you'll often need the index to solve a problem. Let's use this additional info to fix our function now:

js/scripts.js
function boldPassage(word, text) {
  let htmlString = "<p>";
  let textArray = text.split(" ");
  textArray.forEach(function(element, index) {
    if (word === element) {
      htmlString = htmlString.concat("<b>" + element + "</b>");
    } else {
      htmlString = htmlString.concat(element);
    }
    if (index !== (textArray.length - 1)) {
      htmlString = htmlString.concat(" ");
    }
  });
  return htmlString + "</p>";
}

We simply add index as the second parameter in our Array.forEach() loop. Note that the index must be the second parameter. The first parameter is always the current element we are looping through. We don't have to call the parameters element and index - but at least for index, that's generally going to be the best name for this parameter because it describes exactly what it is.

Then we can add a conditional at the end of the loop:

if (index !== (textArray.length - 1)) {
  htmlString = htmlString.concat(" ");
}

If the index that we get from Array.forEach(function(element, index) matches textArray.length - 1, we know we've reached the last element of the array. We should only concatenate a space if it's not the last element of the array.

So now our function will be working correctly. All done, right? Well, no. This function has the same problem that our numberOfOccurrencesInText() function initially had. We need to account for differences in case, punctuation, and so on. So we should just write some more tests and complete that functionality, don't you think? Not so fast. In the next lesson, we are going to discuss another essential programming concept called DRY, which is an acronym for Don't Repeat Yourself.

Terminology

Separation of concerns: A key programming concept that dictates that "concerns" in an application should be separated. For instance, one function might be "concerned" about one thing (adding two numbers together) while another function might be "concerned" with returning those numbers to the user.

Using an index with Array.prototype.forEach()

We can pass in an index as the second parameter of the callback we pass into Array.prototype.forEach(). This allows us to get the index of the current iteration of the loop. The index always starts at 0. Here's an example:

const string = "I like cats!";
string.split(" ").forEach(function(element, index) {
  console.log(element, index);
});

Lesson 6 of 15
Last updated more than 3 months ago.