Include partial templates in Handlebars

Recently at work, we wanted a platform-agnostic way to document our stylesheets. Whatever we chose had to work with Drupal and React while sharing the same documentation format. We settled on kss-node because it was easy to read, easy to maintain, and is platform-agnostic. KSS Node also allows us to choose our templating engine. We could either choose Twig (which Drupal uses) or Handlesbars (which is more JavaScript friendly). Nearly all our developers worked with JavaScript; so we chose Handlerbars.

As time went on we started adding html and the comments were becoming longer. Plus, the comments were unable to contain some documentation we wanted to include such as instructions and guidance with code blocks. We started looking for solutions around the community but none seemed to match our requirements.

Luckily for us KSS Node is very customizable. The theme, or builder, can be changed to include additional information from the SASS comments or to render output from helpers. We decided to use these helpers to retrieve additional documentation or markup from a partial. These partials could be organized to match our url structure, retrieve markup to display component documentation, and run our existing custom helpers without cluttering our SASS comments too much.

Creating the "includes" Helper#

Now that we had a path forward, we started gathering requirements for this new “includes” helper.

  1. Can be used in the SASS comment
  2. Extract html markup from a .hbs file with a relative path from the main index.hbs file
  3. Still allow plain text from the description

Register the Helper

First, we needed to let KSS Node to look for the new helper. Every builder has a kss-config.json file. This file handles important configuration for your kss-node style guide such as additional stylesheets, title of the site, the location of the homepage file, and the names of custom helper. Below is the contents of the kss-config.json file from the Michelangelo theme.

    
JAVASCRIPT
{ "title" : "Michelangelo Styleguide", "mask" : "*.scss", "placeholder" : "[modifier]", "//": "relative to this file.", "builder" : "kss_styleguide/custom-template/", "source" : "src/", "destination" : "kss_styleguide/styleguide/", "//": "relative to source.", "homepage" : "../kss_styleguide/kss-homepage.md", "//": "relative to the generated style guide.", "css": [], "js" : [] }

In order to register the custom helper(s), add the “custom” key to the configuration object.

    
JAVASCRIPT
"custom": [ "includes" ]

In our case, the custom helper(s) will have custom logic in individual files. We want these files to be in their own folder for organization reasons. Underneath the “custom” key, add a new key for “extend”.

    
JAVASCRIPT
"extend": "kss_styleguide/extend",

This value will designate a folder (relative to the root of the project) for KSS Node to look up these helper files.

The final result including these changes (without the comments) would be:

    
JAVASCRIPT
{ "title" : "Michelangelo Styleguide", "mask" : "*.scss", "placeholder" : "[modifier]", "builder" : "kss_styleguide/custom-template/", "source" : "src/", "destination" : "kss_styleguide/styleguide/", "homepage" : "kss_styleguide/kss-homepage.md", "custom": [ "includes" ], "extend": "kss_styleguide/extend", "css": [], "js" : [] }

The styleguide may have to be re-compiled to see changes.

Create the helper

In the folder designated for extending the styleguide, create a file named includes.js. The filename doesn’t have to be the same as the helper’s name; but it helps with organization.

    
JAVASCRIPT
const fs = require("fs"); const jsdom = require("jsdom"); module.exports = function (Handlebars) { "use strict"; Handlebars.registerHelper("includes", function(filename) { if (typeof filename !== 'undefined') { return; } const cwd = process.cwd(); const url = `${cwd}/kss_styleguide/${filename}`; const includedFiles = fs.readFileSync(url, "utf8"); const { JSDOM } = jsdom; const dom = new JSDOM(includedFiles); const domContent = dom.window.document.body.innerHTML; const domTemplate = Handlebars.compile(domContent); return new Handlebars.SafeString(domTemplate({})); }); };

Let’s unpack the code block above.

    
JAVASCRIPT
const fs = require("fs"); const jsdom = require("jsdom"); module.exports = function (Handlebars) { "use strict"; Handlebars.registerHelper("includes", function(filename) { // Helper code goes here... }); };

We’re requiring the fs (filesystem) to find the file based on the passed filename parameter and the jsdom node module to access the DOM (Document Object Model) within the file.

Next, we pass Handlebars in the export function. This function will send the code inside this function to KSS Node the next time the styleguide is compiled.

Inside the export function we register the helper in Handlebars with the name “includes” and pass one parameter for the filename. The next step will explain the code inside the Handlebar’s registerHelper function.

    
JAVASCRIPT
const cwd = process.cwd(); const url = `${cwd}/kss_styleguide/${filename}`; const includedFiles = fs.readFileSync(url, "utf8");

Our build system runs from the root of the project; so we need to retrieve the current working directory using process.cwd() to begin building the path to the file we want to retrieve. Then, we finish building the url with base path of our styleguide (kss_styleguide in this example) and the filename passed in the parameter.

Using the fs node module’s readFileSync function, we create a file stream to use with jsdom.

    
JAVASCRIPT
const { JSDOM } = jsdom; const dom = new JSDOM(includedFiles);

Not much here other than initializing JSDOM and passing the includedFiles file stream to the JSDOM object.

    
JAVASCRIPT
const domContent = dom.window.document.body.innerHTML; const domTemplate = Handlebars.compile(domContent); return new Handlebars.SafeString(domTemplate({}));

Finally, we use JSDOM to pull the HTML from the file. Handlebars is very picky with markup and how it should be rendered. We need to tell Handlebars the string of markup is safe; so we use Handlebar’s SafeString function to tell Handlebars and KSS Node the content is safe to render. Even with the SafeString function we need to compile the markup string first with Handlebar’s compile function.

Using the Helper

Now that we’ve created the helper, we can use it in our main template. When calling helpers, the name of the helper goes first and then the parameters.

    
JAVASCRIPT
{{ includes path/to/partial.hbs }}

In this case, our helper name is includes and we pass the filename parameter as path/to/partial.hbs. Not an actual path or filename but it’s an easy example.

How do we check if the filename has the extension we’re expecting? I’m glad you asked…

Bonus: Create a Conditional Expression Helper#

We wanted to include these partial files in the description of the style sheet’s code blocks. However, not every one will have a partial file. How do we check in our styleguide template if the description is a partial or a block of text?

Handlebars doesn’t provide a good way out of the box; but, as we know now, Handlebars and KSS Node are very customizable.

    
JAVASCRIPT
module.exports = function (Handlebars) { "use strict"; Handlebars.registerHelper("contains", function(needle, haystack, options) { needle = Handlebars.escapeExpression(needle); haystack = Handlebars.escapeExpression(haystack); return (haystack.indexOf(needle) > -1) ? options.fn(this) : options.inverse(this); }); };

Above, we created another helper named contains. The idea being if a string “contains” a substring then the condition returns true. The helper’s parameters has needle as the substring search, haystack as the string to search inside, and options to allow us to access the code inside the condition block.

    
JAVASCRIPT
needle = Handlebars.escapeExpression(needle); haystack = Handlebars.escapeExpression(haystack);

We need to escape the parameters before checking if the string contains the substring.

    
JAVASCRIPT
return (haystack.indexOf(needle) > -1) ? options.fn(this) : options.inverse(this);

Finally, we return a boolean if the string contains the substring or not. If it returns true then the code within the contains block will run. Otherwise, the inverse code in the else block will execute.

Below is an example of how our styleguide checks if the description from our Sass comment contains a .hbs partial. If .hbs is present then we use our includes helper we created earlier to retrieve the contents of the file. All other descriptions will just display as it appears in the style sheet comment.

    
JAVASCRIPT
{{#if description}} <div class="kss-description"> {{#contains ".hbs" description}} {{ includes description }} {{else}} {{{description}}} {{/contains}} </div> {{/if}}

If you haven’t seen triple curly brackets in Handlebars before, it tells Handlebars to escape the text as much as possible. In other words, it’s safe.

Below is an example of a style sheet code block containing a .hbs partial file.

    
SCSS
// Accordion // // partials/components/accordion.hbs

Don’t forget to add contains to the extend section of your kss_config.json file. Including this new helper into the kss_config.json file from before would look like:

    
JAVASCRIPT
{ "title" : "Michelangelo Styleguide", "mask" : "*.scss", "placeholder" : "[modifier]", "builder" : "kss_styleguide/custom-template/", "source" : "src/", "destination" : "kss_styleguide/styleguide/", "homepage" : "kss_styleguide/kss-homepage.md", "custom": [ "contains", "includes" ], "extend": "kss_styleguide/extend", "css": [], "js" : [] }

For more information about KSS Node schema, you can read their documentation page.

If you’re interested in reading more about KSS Node, I highly recommend this tutorial at CSS Tricks.