Chapter 7: Table of Contents. Sections. Sidebar. Toggle TOC. Highlight for section. Active section. Hide Header. Mobile browser.
The section below is a preview of Builder Book. To read the full text, you will need to purchase the book.
In Chapter 7, you'll start with the codebase in the 7-begin folder of our builderbook repo and end up with the codebase in the 7-end folder. We'll cover the following topics in this chapter:
Table of Contents - Sections - Sidebar - Toggle TOC
Highlight for section - Active section
Hide Header
Mobile browser
In Chapter 5, you built the ReadChapter page, which displays the content of one chapter. In this chapter, we will make multiple improvements to this page.
For example, a user should be able to navigate between chapters and between sections within one chapter. To achieve that, we need to introduce a Table of Contents (TOC). The TOC should contain:
- hyperlinked sections within each chapter
- hyperlinked titles of all chapters
Here's an example of what the TOC would look like once we implement it:: 
Table of Contents link
On a high level, we will add the TOC to our ReadChapter page in two main steps. We will discuss and write:
renderSectionsmethod that returns a list of hyperlinked sections within one chapterrenderSidebarmethod that returns a list of hyperlinked titles for all chapters and includesrenderSectionsunder each chapter's title
We will add the renderSidebar method inside the ReadChapter.render method and then test out the TOC.
Sections link
In this subsection, we define the renderSections method. This function returns a list of hyperlinked sections for one chapter.
Recall how we defined the sections parameter in our Chapter model. Open server/models/Chapter.js and look at the schema:
sections: [
{
text: String,
level: Number,
escapedText: String,
},
],Sections is an array of objects, and each object has text, level, and escapedText. Where do these three parameters come from? In the same server/models/Chapter.js file, find how we generate the sections array:
const sections = getSections(content);
Definition of getSections method:
function getSections(content) {
const renderer = new marked.Renderer();
const sections = [];
renderer.heading = ({ text }, level) => {
if (level !== 2) {
return;
}
const escapedText = text
.trim()
.toLowerCase()
.replace(/[^\w]+/g, '-');
sections.push({ text, level, escapedText });
};
marked.setOptions({
renderer,
});
marked(he.decode(content));
return sections;
}marked parses markdown content and finds headings with level equal to 2 (any heading that has ##). For every heading, we push an object to the sections array:
sections.push({ text, level, escapedText })In other words, if your markdown content has:
## Why this book?Then getSections(content) will return this array:
[
{
"text": "Why this book?",
"level": 2,
"escapedText": "why-this-book-"
},
]We will use text as text inside <a>{text}</a> and use escapedText for href. When a user clicks on a hyperlinked section inside the TOC, we want the page to scroll to the beginning of that section. In fact, when we wrote our markdownToHtml method in Chapter 6, we defined the <h2> heading as follows:
if (level === 2) {
return `<h${level} class="chapter-section" style="color: #222; font-weight: 400;">
<a
class="section-anchor"
name="${escapedText}"
href="#${escapedText}"
style="color: #222;"
>
<i class="material-icons" style="vertical-align: middle; opacity: 0.5; cursor: pointer;">link</i>
</a>
${text}
</h${level}>`;
}Let's say we have the heading ## Why this book? in our markdown content. When user a clicks on the link icon next to that heading, two things will happen: - the URL in the browser address bar gets #why-this-book- appended to it - the page scrolls to the beginning of that section because <a> has the name attribute
We want to get the exact same behavior when a user clicks on the hyperlinked section text inside our TOC. Thus we get:
<a href={`#${s.escapedText}`}>
{s.text}
</a>Keep in mind that sections is an array. Thus, you should use JavaScript's method Array.map:
<ul>
{sections.map((s) => (
<li key={s.escapedText} style={{ paddingTop: '10px' }}>
<a href={`#${s.escapedText}`}>
{s.text}
</a>
</li>
))}
</ul>You already used this method earlier in this book. Check out books.map inside pages/admin/index.jsx.
At this point, the main part of our renderSection method is actually done.
Open your pages/public/read-chapter.jsx file. Remember that we defined the chapter object isnide the page with:
const chapter = await getChapterDetailApiMethod({ bookSlug, chapterSlug }, { headers });And then we set the initial state object of our ReadChapter page component with the following properties:
this.state = {
chapter,
htmlContent,
};Now let's use ES6 object destructuring to define sections as this.state.chapter.sections:
const { sections } = this.state.chapter;Also, let's return null if the section array does not exist or the array has zero members:
if (!sections || !sections.length === 0) {
return null;
}Put together these two code snippets, plus return a list of hyperlinked sections, and you get:
renderSections() {
const { sections } = this.state.chapter;
if (!sections || !sections.length === 0) {
return null;
}
return (
<ul>
{sections.map((s) => (
<li key={s.escapedText} style={{ paddingTop: '10px' }}>
<a href={`#${s.escapedText}`}>
{s.text}
</a>
</li>
))}
</ul>
);
}Good job if you got the same result. We'll add this to our ReadChapter page at the end of the next subsection.
Sidebar link
In this subsection, we discuss the renderSidebar method that returns a list of hyperlinked titles for all chapters. We will then add our renderSection method from above to add a list of all sections under each chapter list item. Together, chapters and their sections, make up the Table of Contents (TOC).
Similar to how we defined the renderSections method, we will use list, list item, and anchor elements together with the JavaScript method Array.map for our new renderSidebar method:
renderSidebar() {
return (
<div>
<p>{book.name}</p>
<ol>
{chapters.map((ch, i) => (
<li key={ch._id} role="presentation">
<Link
as={`/books/${book.slug}/${ch.slug}`}
href={`/public/read-chapter?bookSlug=${book.slug}&chapterSlug=${ch.slug}`}
>
<a>{ch.title}</a>
</Link>
</li>
))}
</ol>
</div>
);
}The only differences with renderSections are: - Instead of an unordered list <ul>, we use an ordered list <ol>. - We wrap the anchor <a> with Next.js's <Link>, so we can, later, take advantage of the default prefetch feature in production. - We also display the book name on the top of the TOC with <p>{book.name}</p>.
You've reached the end of the Chapter 7 preview. To continue reading, you will need to purchase the book.