/table-of-contents

A walk-through using DOM methods to auto-generate a table of contents

Primary LanguageHTMLISC LicenseISC

Walk-Through: Auto Table of Contents

Join the chat at https://gitter.im/unioncollege-webtech/table-of-contents

In this walk-through, we are going to write JavaScript to auto-generate a table of contents for an article based on the headings in the HTML document. Along the way we’ll be learning and using several vital DOM methods.

We will be working with a modified version of the ZooKeep assignment description. The source HTML that we’re working with is in index.html.

“index.html” already contains a script element loading toc.js. The code examples below should be added to “toc.js” to auto-generate our table of contents.

Step 1: Find the heading elements

Use document.querySelectorAll to find all the headings in the article except for the h1 (i.e. h2, h3, h4, h5, h6).

var headings = document.querySelectorAll("h2, h3, h4, h5, h6");

Just to ensure that our code is working as expected so far, use console.log to display headings in the browser’s console.

console.log(headings);

At this point, if you haven’t already, open “index.html” in a browser. Activate the Developer Tools and the Console. Refresh the page and make sure the NodeList of heading elements is logged to the console. You should see something like this, though the exact output varies by browser:

▶ [h2#description, h3#create-zoo-js-with-the-animals-array, ...]

Step 2: Create an ordered list

Our table of contents will be an ordered list of links to the individual sections of our document. The user will be able to click on these links to jump directly to specific sections.

Use document.createElement to create a new ol element that will hold the table of contents list.

var tocList = document.createElement('ol');

Again, to ensure that our code is functioning correctly, log tocList to the console.

console.log(tocList);

Step 3: Loop through the headings

Loop through the headings using a for loop and build a list of links to the different sections of the document. The final HTML for the table of contents list will look something like the following:

<ol>
  <li>
    <a href="#description" rel="internal">Description</a>
  </li>
  <li>
    <a href="#create-zoo-js-with-the-animals-array" rel="internal">Create zoo.js with the animals array</a>
  </li>
  <li>
    <a href="#sort-and-display-the-animal-gallery" rel="internal">Sort and display the animal gallery</a>
  </li>
  <li>
    <a href="#display-the-featured-animal" rel="internal">Display the featured animal</a>
  </li>
  ...
</ol>

To generate the content above, we will use the id attribute from the headings to generate the fragment identifier for the href attribute on the a elements, and use the heading text for the a element’s content.

Just so we are certain that we have access to the necessary information, let’s start by creating a for loop that just logs the id and textContent properties for each heading in our headings list.

for(var i = 0, len = headings.length; i < len; i++) {
    var heading = headings[i];
    
    console.log(heading.id);
    console.log(heading.textContent);
}

At this point, save “toc.js” and load “index.html” in the browser. Refresh the page and ensure that the correct information is being logged to the console. It should look something like the following:

description \n Description \n create-zoo-js-with-the-animals-array \n Create...

Step 4: Build the a and li elements

Now that we know we have access to the id and textContent of the headings, it’s short work to build the list item and anchor elements for each of the headings. For each of the headings, create a li (list item) element and an a (anchor) element. Set the href, rel, and textContent properties of the anchor, then use node.appendChild to add the anchor element to the list item, then add the list item to the tocList ordered list element.

for(var i = 0; i < headings.length; i++) {
    var heading = headings[i];
    
    // console.log(heading.id);
    // console.log(heading.textContent);
    
    // Create the `li` and `a` elements
    var li = document.createElement('li');
    var a = document.createElement('a');

    // console.log(a);
    // console.log(li);
    
    // Set the necessary properties on the `a` element
    a.href = '#' + heading.id;
    a.textContent = heading.textContent;
    a.rel = 'internal';

    // Append the `a` element to the `li`
    li.appendChild(a);

    // console.log(li);
    
    // Append the `li` to the `tocList` (an `ol` element)
    tocList.appendChild(li);
}

Just to be certain that our table of contents list is looking okay, let's log it to the console.

console.log(tocList);

Step 5: Find the Table of Contents placeholder element

In index.html line 17 you’ll see that we have a nav element element with the id “table-of-contents”. This element is a placeholder for the table of contents list that we are building.

Use document.querySelector to find the element with the id “table-of-contents”. Store this in a variable called toc.

var toc = document.querySelector('#table-of-contents');

To be certain that we found the correct element, log toc to the console.

console.log(toc);

Step 6: Add the tocList to the toc placeholder element

All that’s necessary now is to add tocList to toc by using appendChild.

toc.appendChild(tocList);

Take a look at “index.html” in a browser now, and you see the table of contents list right below the introductory paragraph. Woot!!

Screenshot of the list of links below the introductory paragraph

Step 7: Add a “Table of Contents” heading

It would be helpful if we had a heading for the table of contents list. Use document.createElement to generate an h2 element with the text “Table of Contents”.

var tocHeading = document.createElement('h2');
tocHeading.textContent = 'Table of Contents';

To be certain that our heading is correct, log it to the console.

console.log(tocHeading);

Now, use node.insertBefore to add the heading to the toc placeholder before the ol containing the list of internal links.

toc.insertBefore(tocHeading, tocList);

Take a look at “index.html” in a browser. Your table of contents list should look fantastic.

Screenshot of the table of contents list with heading

Step 8: Make it collapsible

It would be a nice enhancement to allow the user to click on the Table of Contents heading to collapse or expand the list.

Use addEventListener to listen for ‘click’ events on the table of contents heading. In the listener callback function, use the classList property on tocList to toggle the ‘collapsed’ class name.

tocHeading.addEventListener('click', function(){
    console.log('Heading clicked!');
    tocList.classList.toggle('collapsed');
    console.log(tocList);
}, false);

Currently we are only adding and removing a class name, which doesn’t have any visible affect on the table of contents list. We can view the console or use Inspect Element to see that the class attribute is being changed, but we don’t have the necessary CSS rules to actually collapse the table of contents list when the heading is clicked.

In style.css, add a rule which sets the display property to none for elements that have the ‘collapsed’ class name.

.collapsed {
    display: none;
}

Now take a look at “index.html” in the browser, and things should be much more exciting.

Step 9: Celebrate!

Wooot!!! We just made an auto-generated collapsible table of contents for our article! That’s pretty awesome.

Step 10: Additional improvements

Nested Table of Contents

Currently our table of contents list is flat: h2 headings are on the same level as h3s. In a true outline, h3 elements that follow an h2 are considered children of the h2 elements and should be nested one level deeper.

Update the for loop that generates the list items so greater heading levels are appropriately nested. Your ending HTML structure should look something like the following:

<ol>
  <li>
    <a href="#description" rel="internal">Description</a>
    <ol>
      <li>
        <a href="#create-zoo-js-with-the-animals-array" rel="internal">Create zoo.js with the animals array</a>
      </li>
      <li>
        <a href="#sort-and-display-the-animal-gallery" rel="internal">Sort and display the animal gallery</a>
      </li>
      <li>
        <a href="#display-the-featured-animal" rel="internal">Display the featured animal</a>
      </li>
      <li>
        <a href="#map-sort-and-display-animal-ages" rel="internal">Map, sort, and display animal ages</a>
      </li>
    </ol>
  <li>
    <a href="#animals" rel="internal">Animals</a>
  </li>
  ...
</ol>

The code necessary for this is left as an exercise for you.

Improved styles for collapse/expand

It’s currently not possible to tell that the Table of Contents heading is clickable to collapse or expand the list. Make the necessary changes to “styles.css” and the heading’s ‘click’ listener to make the collapse/expand functionality more obvious.

Improve the structure for toc.js

Currently we are creating a lot of variables (headings, toc, tocList, tocHeading, i, heading, a, li). All of these variables are attached to the global scope by default. That’s bad.

In “toc.js”, declare a new function (called autoToC or something similar) and put all of the code from “toc.js” into the body of the function. Then at the end of the file, call the autoToC function. Your final code might look something like this:

function autoToC() {
    // Find the heading elements
    var headings = document.querySelectorAll("h2, h3, h4, h5, h6");
    console.log(headings);

    // Create an ordered list
    var tocList = document.createElement('ol');
    console.log(tocList);

    // Loop through the headings
    for (var i = 0; i < headings.length; i++) {
        var heading = headings[i];
        // ... (trimmed)
    }
    // ...
}

autoToC();

Note: it’s actually more helpful if the “toc.js” file doesn’t call itself directly, but instead just defines the function. It would probably be most helpful if the autoToC() function created and returned the nav element. This would give the caller complete control over where the table of contents is placed in the document. For instance “index.html” could use it like so:

<script>
    var description = document.querySelector('#description');
    description.parentNode.insertBefore(autoToC(), description);
</script>

Consider making the changes necessary for this, but it is not required.

Completing and submitting the assignment

You are also welcome commit, push, and create a pull request before you’ve completed your solution. You can ask questions or request feedback there in your pull request. Just mention @barberboy in your comments to get my attention.

References

License

ISC