By
/07.11.23

MacBook pro displaying some HTML code.

It is sometimes impossible to render HTML directly in Rails, for example, when you need to procedurally generate HTML from JavaScript. There are some approaches, but one sticks out: Template tags && templating engine. In this blog, I will explain how a JavaScript developer can render HTML and how to use the knowledge with a Stimulus example from an actual application. We will also look at how to use Mustache.js for this exact purpose.

Existing approaches to HTML in JS

1. Template Strings

  const name = 'John';
const greetingHTML = `<div id="greeting">
  <h1>Hello ${name}</h1>
</div>`;

document.body.insertAdjacentHTML(greetingHTML, 'beforeend');

As addressed in this article by Tristan Forward, template strings are the fastest approach to rendering HTML with JavaScript. It is also quite readable but comes with some disadvantages, mainly:

  1. Missing intelligence/linting: You most likely need an IDE to correctly highlight HTML code inside of a template string, and even with linting, it most likely won't get checked

  2. Lack of Consistency: Because you most likely cannot use your HTML / erb linters on template strings, you lose consistency between your templates and your regular HTML

  3. Maintainability: Having templates spread around in your JavaScript controllers can pose a headache compared to a more centralized way. Yes, you can abstract it away in a separate template class, but it is a lot of unnecessary work.

2. Creating elements manually with document.createElement()

The second approach is quite old-school, involves A LOT more work, and is quite painful, but it is often considered better practice as it is more explicit (and has better IE6 support). It is also the approach most developers are familiar with:

  const greetingWrapper =  document.createElement('div');
greetingWrapper.setAttribute('id', 'greeting');

const headingElement = document.createElement('h1');
headingElement.innerHTML = `Hello ${name}`;

greetingWrapper.appendChild(headingElement);

The main downside is that this approach is not as descriptive as plainHTML™. If you don't like wasting precious brain power just to read code like that, primarily when the HTML is spread across multiple methods/classes, you need to find a different solution. Yucky!

3. Use an HTML element as a template + interpolate values with the query Selector

  <template id="greetingTemplate">
  <div class="greeting">
    <h1>Hello {{name}}</h1>
  </div>
</template>

Have you never seen a template tag before?

If you are questioning your HTML knowledge because you are seeing another tag for the first time, don't because the <template> tag is not used that often but still has a purpose:

The <template> HTML element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template

Back to the topic

Note that the text Hello {{name}} will be replaced entirely, and its only purpose is to improve readability.

  const greetingTemplate = document.getElementById('greetingTemplate').cloneNode(true);
const greetingWrapper = greetingTemplate.firstChild;
const heading = greetingWrapper.querySelector('h1');

const name = 'John';
heading.innerText = `Hello ${name}`;
document.body.appendChild(greetingWrapper);

The main problem with this approach is the query selector. This is considered an expensive operation and can be lengthy when replacing many values 😓.

4. Use an HTML element as a template + interpolate values with a templating engine (🥇THE BEST APPROACH🥇)

If you want to avoid writing your own template engine (like I did before discovering Mustache) to handle the value replacements yourself, consider using a library. This is where Mustache comes into play.

You first declare your HTML element and then use {{}} to signify the placeholder values:

  <template id="greetingTempltae">
  <div class="greeting">
    <h1>Hello {{name}}!</h1>
  </div>
</template>

Then, you can put in values from a hash in JavaScript to render the HTML:

  const view = {
  name: "John"
}

const template = document.getElementById("greetingTemplate").innerHTML

const greetingHTML = Moustache.render(template, {
name: "John"
})

document.insertAdjacentHTML(greetingHTML, 'beforeend')

The benefits of this approach:

  1. Readability: The usage of template strings and a template engine is very readable, as you have a perfect overview of the hierarchy, especially in more complex partials

  2. Separation of concerns: It is generally bad practice to render HTML from template strings in JavaScript, so extracting the HTML from JavaScript and only interpolating the values is much better.

  3. It is effortless! 🎉

Mapbox Example

In this example, I will show you how I used Moustache JS in my typescript + rails application to create radio buttons for Mapbox styles. To DRYify (Do not Repeat Yourself) the application, I decided to generate the radio buttons in JavaScript instead of Rails for better maintainability, as the styles are purely for the client side.

Context

Mapbox styles are like Google Maps layers (terrain, traffic, biking). These radio buttons will be used to switch the various views. Currently, the application consists of the following styles: Satellite and Outdoor. But may get new map styles, like Swisstopo tiles, for example.

The data

I created the following style object to store the style URL and corresponding icon:

  export type MapboxStyle = {  
  url: string  
  icon: string  
}

export const MAPBOX_STYLES: Record<string, MapboxStyle> = {  
  outdoor: {  
    url: 'mapbox://styles/...',  
    icon: 'fa fa-hiking',  
  },  
  satellite: {  
    url: 'mapbox://styles/...',  
    icon: 'fa fa-satellite',  
  },  
}

export const DEFAULT_MAPBOX_STYLE_KEY = 'outdoor'
export const DEFAULT_MAPBOX_STYLE = MAPBOX_STYLES[DEFAULT_MAPBOX_STYLE_KEY]

The HTML

This is the template I am using to create the individual radio buttons:

  <div id="map-wrapper" class="map__wrapper"
     data-controller="map markerRelocation mapboxLayers"
     data-map-locations-path-value="<%= locations_url(format: :json) %>"
     data-markerRelocation-map-outlet="#map-wrapper"
     data-mapboxLayers-map-outlet="#map-wrapper"
     data-map-access-token-value="<%= ENV.fetch('MAPBOX_ACCESS_TOKEN') %>">
  <div id="map" class="map__container flex-grow-1"></div>
  <div data-mapboxLayers-target="mapboxStyleRadioWrapper" class="mapbox-style-radio__wrapper"></div>

  <script data-mapboxLayers-target="mapboxStyleRadioTemplate" type="x-tmpl-mustache">
    <div class="mapbox-style-radio">  
      <input id="{{radio_id}}" class="form-check-input" type="radio" name="map-style"
            value="{{mapbox_style_url}}"
            data-action="mapboxLayers#swapLayer"
            {{#checked}} checked {{/checked}}>  
      <label for="{{radio_id}}" class="form-check-label">  
        {{style_name}}  
        <i class="fa-solid {{icon_class}}"></i>  
      </label>  
    </div>  
  </script>
</div>

Interpolating the values

In a previous section, we created the corresponding HTML template for the radio button with individual template attributes. To render the template, we will first get the inner HTML of the template tag and then use Mustache.js to generate the interpolated string:

  import { Controller } from '@hotwired/stimulus'
import MapController from './map_controller'

export default class MapboxLayers extends Controller {
  static outlets = ['map']

  static targets = ['mapboxStyleRadioTemplate', 'mapboxStyleRadioWrapper']

  declare readonly mapOutlet: MapController

  declare readonly mapboxStyleRadioTemplateTarget: HTMLTemplateElement

  declare readonly mapboxStyleRadioWrapperTarget: HTMLDivElement

  connect() {
    this.createLayers()
  }

  createLayers() {
    this.mapboxStyleRadioWrapperTarget.innerHTML = ''

    Object.keys(MAPBOX_STYLES).forEach((key) => {
      const { url, icon } = MAPBOX_STYLES[key]
      
      const options = {
        mapboxStyleUrl: url,
        iconClass: icon,
        radioId: `mapbox-style-radio-${key}`,
        styeleName: I18n.map[`style_${key}`],
        checked: DEFAULT_MAPBOX_STYLE_KEY === key
      }

      const htmlString = Mustache.render(template, options)

      this.mapboxStyleRadioWrapperTarget.insertAdjacentHTML('beforeend', htmlString)
    })
  }

  swapLayer(event: Event) {
    const target = event.target as HTMLInputElement
    const styleURL = target.value

    if (!styleURL) throw new Error('Style url not specified on element')

    this.mapOutlet.map.setStyle(styleURL)
  }
}

Wait, why is it a <script> tag now?

Checking radio buttons or adding boolean attributes is generally a pain in JavaScript, mainly because checked="false" and checked="true" have the same effect. The boolean attribute must be present for the attribute to be active.

The <template> HTML element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template

It is worth noticing that a boolean attribute is true when it is present and false when it is absent.

https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML

So, anyway, I am using a <script> tag for my template to use special Moustache interpolation. The attribute checked gets added when options["checked"] is true, and that's it! This is the code I am referring to in particular:

  ...
{{#checked}} checked {{/checked}}>

Conclusion

This article covers different approaches to rendering HTML in JavaScript and recommends using template tags and a templating engine, particularly Mustache.js, for optimal readability, maintainability, and separation of concerns within a Rails application. I demonstrated how this method can be implemented with a real-world example involving radio buttons for Mapbox styles, emphasizing the advantages of using this approach.