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:
renderSections
method that returns a list of hyperlinked sections within one chapterrenderSidebar
method that returns a list of hyperlinked titles for all chapters and includesrenderSections
under 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.