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 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: The finished code is available on GitHub if you just want to get right to using it. 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 What may not be clear in the code-block element above is There are several ways to style web components, but we're going to use a Notice the Nice work! This is already starting to come along. 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 We're going to edit this newly downloaded JS file and add The raw tag ensures that any braces in the JavaScript are not evaluated by the HubL renderer. Next, we download the Normally, if we want to load CSS or JS we would use the HubL Inside of our 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 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: Something I also want to handle is escaping the code. “Escaping the code” means that any HTML code you place between 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 We'll use three data attributes on the Technically speaking, the browser would let us write the attributes like this: 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 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 We need to add the logic for encoding the HTML. Above that code tab class, we're going to add the function for We're so close to being done, and the hard part is over. We're now going to build out our 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 Now, we're going to add logic to generate a 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!
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.code-block
element that will be an outer wrapper element into which we will place our code-tab elements: This code-block
element will be able to hold multiple code-tab
elements. It will generate a tabbed interface based on the code-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.Step 1: Create a Component to Display Code
pre
tag — and, to keep it semantic, we'll place a code tag inside.
. 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. tag inside the inner HTML to keep our web component code together.
: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. Step 2: Add Syntax Highlighting to the Code Tabs
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.)raw
to the top of the code and endraw
to the bottom.=0&&b(p,"variable-input")}}}}function l(n){return t[e+n]}function c(n,t){t=t||0;for(var e=0;e
",document.body.appendChild(t),e=38===t.offsetHeight,document.body.removeChild(t)}return e}()?parseInt:parseFloat)(getComputedStyle(r).lineHeight),f=Prism.util.isActive(r,t),p=r.querySelector("code"),g=f?r:p||r,m=[],v=p&&g!=p?function(e,t){var i=getComputedStyle(e),n=getComputedStyle(t);function r(e){return+e.substr(0,e.length-2)}return t.offsetTop+r(n.borderTopWidth)+r(n.paddingTop)-r(i.paddingTop)}(r,p):0;c.forEach((function(e){var t=e.split("-"),i=+t[0],n=+t[1]||i,o=r.querySelector('.line-highlight[data-range="'+e+'"]')||document.createElement("div");if(m.push((function(){o.setAttribute("aria-hidden","true"),o.setAttribute("data-range",e),o.className=(u||"")+" line-highlight"})),f&&Prism.plugins.lineNumbers){var s=Prism.plugins.lineNumbers.getLine(r,i),l=Prism.plugins.lineNumbers.getLine(r,n);if(s){var a=s.offsetTop+v+"px";m.push((function(){o.style.top=a}))}if(l){var c=l.offsetTop-s.offsetTop+l.offsetHeight+"px";m.push((function(){o.style.height=c}))}}else m.push((function(){o.setAttribute("data-start",String(i)),n>i&&o.setAttribute("data-end",String(n)),o.style.top=(i-d-1)*h+v+"px",o.textContent=new Array(n-i+2).join(" \n")}));m.push((function(){o.style.width=r.scrollWidth+"px"})),m.push((function(){g.appendChild(o)}))}));var y=r.id;if(f&&Prism.util.isActive(r,i)&&y){s(r,i)||m.push((function(){r.classList.add(i)}));var b=parseInt(r.getAttribute("data-start")||"1");o(".line-numbers-rows > span",r).forEach((function(e,t){var i=t+b;e.onclick=function(){var e=y+"."+i;n=!1,location.hash=e,setTimeout((function(){n=!0}),1)}}))}return function(){m.forEach(l)}}};var r=0;Prism.hooks.add("before-sanity-check",(function(e){var t=e.element.parentElement;if(a(t)){var i=0;o(".line-highlight",t).forEach((function(e){i+=e.textContent.length,e.parentNode.removeChild(e)})),i&&/^(?: \n)+$/.test(e.code.slice(-i))&&(e.code=e.code.slice(0,-i))}})),Prism.hooks.add("complete",(function e(i){var n=i.element.parentElement;if(a(n)){clearTimeout(r);var o=Prism.plugins.lineNumbers,l=i.plugins&&i.plugins.lineNumbers;s(n,t)&&o&&!l?Prism.hooks.add("line-numbers",e):(Prism.plugins.lineHighlight.highlightLines(n)(),r=setTimeout(u,1))}})),window.addEventListener("hashchange",u),window.addEventListener("resize",(function(){o("pre").filter(a).map((function(e){return Prism.plugins.lineHighlight.highlightLines(e)})).forEach(l)}))}function o(e,t){return Array.prototype.slice.call((t||document).querySelectorAll(e))}function s(e,t){return e.classList.contains(t)}function l(e){e()}function a(e){return!!(e&&/pre/i.test(e.nodeName)&&(e.hasAttribute("data-line")||e.id&&Prism.util.isActive(e,i)))}function u(){var e=location.hash.slice(1);o(".temporary.line-highlight").forEach((function(e){e.parentNode.removeChild(e)}));var t=(e.match(/\.([\d,-]+)$/)||[,""])[1];if(t&&!document.getElementById(e)){var i=e.slice(0,e.lastIndexOf(".")),r=document.getElementById(i);r&&(r.hasAttribute("data-line")||r.setAttribute("data-line",""),Prism.plugins.lineHighlight.highlightLines(r,t,"temporary ")(),n&&document.querySelector(".temporary.line-highlight").scrollIntoView())}}}();
!function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){var e="line-numbers",n=/\n(?!$)/g,t=Prism.plugins.lineNumbers={getLine:function(n,t){if("PRE"===n.tagName&&n.classList.contains(e)){var i=n.querySelector(".line-numbers-rows");if(i){var r=parseInt(n.getAttribute("data-start"),10)||1,s=r+(i.children.length-1);tprism.css
file and place it in our src/css/components
directory.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. 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.code-tab
element's connectedCallback()
adopting the stylesheet we just imported.
and 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.
code-tab
web component. Passing and retrieving this information works almost the exact same way as it does with a normal HTML element.code-tab
element. It will look like this: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.${code}
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.encode
.${code}
Step 3: Add Tabs and Labeling of the code
web component, which will handle rendering tabs.
to make all of the code tabs we place inside appear below the tabs div
.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