How to Build a Code Block Web Component
Businesses with APIs of all kinds need to be able to document them so they can be leveraged by both customers and partners. HubSpot is no different — we have a lot of API endpoints and a full CMS Hub, which is directly integrated into the HubSpot CRM.
There are a multitude of ways that we want to use formatted code blocks in our documentation and we have multiple custom modules for displaying them.
Maintaining separate code for each module can cause issues, though. Duplicate code makes it harder to maintain. It means we need to update multiple modules at a time in order to ensure feature parity, and any updates that require HTML changes need to be made in all of the modules or we risk breaking live code blocks. We also have multiple teams that can benefit from the code blocks but may need to use them in different environments. In some environments, we may have JavaScript frameworks, in others we don't. We have teams that want to use them in blog posts and our developer documentation.
To solve for the wide array of use–cases, we're turning to web components for this specific task. If you've never used web components before, be sure to read our primer for using web components on HubSpot.
Briefly, however, web components are like custom HTML elements. You build them using JS, HTML, and CSS. Adding a web component to the page is the same as adding a normal HTML element like a <div>
. The web component acts almost like an iframe — CSS and JavaScript in the main page can't affect the code inside the web component unless specifically allowed. This prevents styling conflicts, issues with IDs and classes used for targeting JavaScript, as well as other kinds of conflicts. The shadow DOM is the main way web components are encapsulated (protected from conflicts). While the term “shadow DOM” sounds dark and mysterious, it’s really just the HTML of the web component itself.
The advantage here is we can use the same web component across all of our modules, in our blog posts, and in environments where we're using JavaScript frameworks. In all of these situations, the component will load quickly, and it will look and function the same way. This also consolidates most of the code for displaying the code block into the web component itself, leaving HubSpot modules to just handle fields and render the simple HTML. This means that improvements to the web component can improve all of the places that it gets used simultaneously.
We're technically going to build two web components that are designed to work together:
- A
code-tab
element into which we will place the code we want to visually display: This element will do the actual syntax highlighting and provide a button for site visitors to copy code from. - A
code-block
element that will be an outer wrapper element into which we will place our code-tab elements: Thiscode-block
element will be able to hold multiplecode-tab
elements. It will generate a tabbed interface based on thecode-tab
elements added to it. For example, you could have a tab for HTML and a tab for CSS. We frequently have code-block elements on our site, for instance, showing HubL+HTML and the rendered output of the HubL+HTML.
The finished code is available on GitHub if you just want to get right to using it.
Step 1: Create a Component to Display Code
Let's first get a solid foundation for what the component should look like before adding the syntax highlighting by registering our code-block and code-tab elements.
Then, we'll set the innerHTML for the shadow DOM. This works the same way as setting innerHTML on a normal HTML element, but instead, we just target the shadowRoot. Now, when the browser sees the web component, it will render some HTML.
Next, we'll create some basic HTML structure and then style it. We want our code to maintain its formatting, so we'll use a pre
tag — and, to keep it semantic, we'll place a code tag inside.
What may not be clear in the code-block element above is <slot>
. The term “slot” may be familiar if you've built using a JS framework like React or Vue. It can be thought of as a placeholder that will contain anything you place inside of your web component. So, anything you put inside your newly created code-tab element will appear there in the shadow DOM.
There are several ways to style web components, but we're going to use a <style>
tag inside the inner HTML to keep our web component code together.
Notice the :host
selector above. This selects your new custom element itself so you can style it — it’s kind of like having a wrapper div around your shadow DOM.
Nice work! This is already starting to come along.
Step 2: Add Syntax Highlighting to the Code Tabs
Syntax highlighting is complex and requires parsing the code to break it out into its individual parts. It doesn't make sense to do that from scratch, so what we're going to do is leverage Prism.js, a popular and well–made library for syntax highlighting code.
The website for the project has a convenient build tool you can use to configure exactly what you want and download the code you need. We select the languages we need and then scroll to the bottom where the code is displayed. We're going to download prism.js
and place it in our src/js/
directory. (The directories we're going to mention are based on the HubSpot boilerplate's structure; you can place the scripts in other folders, just keep in mind that you'll need to adjust the paths shown below.)
We're going to edit this newly downloaded JS file and add raw
to the top of the code and endraw
to the bottom.
The raw tag ensures that any braces in the JavaScript are not evaluated by the HubL renderer.
Next, we download the prism.css
file and place it in our src/css/components
directory.
Normally, if we want to load CSS or JS we would use the HubL require_js
and require_css
functions. In this case, though, our Prism code is a dependency of the code-tab
component. Instead of using separate require statements for the component JavaScript, the Prism JavaScript, and Prism CSS, we want to only have one line of code to get the code block going on a page. We could also use webpack to auto-combine the files — and maybe we'll do that in the future to get a tiny performance improvement. But for the sake of keeping this guide approachable for folks of all skill levels, we're going to forgo any build tooling.
Inside of our code-block.js
file, we're going to add two import statements above our code-block and code-tab classes: one import for the Prism stylesheet, and one for the Prism JavaScript.
In order for our web component to use the stylesheet, we need to attach it to the shadow DOM. To do this, we update our code-tab
element's connectedCallback()
adopting the stylesheet we just imported.
We're already close to having syntax highlighting working. Now, we need to determine what kind of information we need to make a code block work. For each code tab we need to know:
- The language of the code in the code tab so the syntax highlighting is correct
- Whether line numbers are desired along the left side to make it easy to reference
Something I also want to handle is escaping the code. “Escaping the code” means that any HTML code you place between <code-tab>
and </code-tab>
will be visually displayed and not rendered on the page. It's typically best to do this server side, and I recommend that in all places where that’s an option.
Because of the potential awkwardness that could be caused by HTML accidentally getting rendered rather than the code being visually displayed, we'll default to assuming the code is not already escaped and provide a way for the person using the component to tell the component they already escaped their code.
That makes three variables we're going to need to pass to our code-tab
web component. Passing and retrieving this information works almost the exact same way as it does with a normal HTML element.
We'll use three data attributes on the code-tab
element. It will look like this:
Technically speaking, the browser would let us write the attributes like this:
<code-tab language="HTML" escaped="true" line-numbers="true">
But if any of those attribute names ever become web standard, that could cause issues. Data attributes protect us from that, as they're developer defined.
Since we're going to pass information that way, we need to get those values so Prism can use them. We actually use getAttribute()
just like we would any other HTML element, the only difference being that we use getAttribute()
on this
which refers to the code-tab itself.
We're also going to include some logic for whether or not the code is escaped.
A few things have changed here. We're getting the following attributes attached to the custom element: language, line-numbers, escaped. We're then using the values in the shadow DOM as classes and the actual code output. We're also telling Prism that once the web component has been initialized, we want it to syntax highlight the code.
You'll notice that where we display our code, we're no longer using a slot directly like <slot></slot>
to output its content where that tag goes. We didn't do that here because we actually want to manipulate that output before displaying it. To do this, we're setting a variable of code
to the innerHTML
of the custom element — then, depending on whether the attribute for escaped is set to true
or false
, we encode special characters preventing HTML from being rendered instead of displayed as text.
We need to add the logic for encoding the HTML. Above that code tab class, we're going to add the function for encode
.
Step 3: Add Tabs and Labeling of the code
We're so close to being done, and the hard part is over. We're now going to build out our <code-block>
web component, which will handle rendering tabs.
First, we're going to create our element and, in its shadow DOM, include a div which will contain our tabs. Then we'll use <slot>
to make all of the code tabs we place inside appear below the tabs div
.
Now, we're going to add logic to generate a button
element within the tabs div for every code-tab element. After we generate those buttons, we add the tab logic needed to hide/show the individual tab's content.
Step 4: The Fun Part: Using the Code Blocks
You can now create code blocks using HTML in the page.
You can then create modules that use the code block HTML.
The GitHub Repository contains a more up-to-date example of how you could use it in a module.
You can also use it in a blog post — instructions for that are in the GitHub repository.
You may have already pieced it together, but every code block in this article — and nearly all of the code blocks on the developers.hubspot.com website — are now powered by this web component. Inspect the page and have a look. The only significant difference between the version you see on GitHub and the HubSpot code block is CSS styling.
We hope you enjoyed this tutorial. As noted, all of the code is available in this GitHub repository. Go make some cool and useful websites!