/ Fly Edge Apps

Edge side JavaScript templates

Today we see the vast majority of websites and apps using JavaScript to create lively, dynamic interfaces. While DOM manipulation is great for some JavaScript apps, what about the apps that constantly need large parts of the document updated every time the view changes? I think we've all experienced how messy and confusing that can get.

Say you have a user fill out a form. Now you want to take their info and display it in an organized manner on the page. You would probably create a form element, listen for a submit, grab the data, get elements from the document, create variables, concatenate strings * breathes * and so on. It would probably look something like this...

document.getElementById("form").addEventListener("submit", function( {document.getElementById("title").innerHTML = document.get .... WAIT WHAT ... what is even going on here anymore?

What if there was an easier, cleaner way to do this? Like when the form is submitted, all of the data just gets injected into the document. No hardcore, chaotic DOM manipulation + string concatenation + programming traumatization.

Well, that’s where JavaScript Templating comes into play.

Templating with Fly

The Fly runtime includes an HTML DOM parser and CSS query engine that are useful for replacing contents of HTML files. You can use the DOM parser to convert strings into structured HTML documents and look for elements to replace with content specified in your JS file. This makes rendering and updating dynamic content on your website an absolute breeze.

Benefits of JavaScript Templating

  • Keeps JavaScript and HTML totally separate. Maintain dead simple HTML files and write JS that updates the contents of those HTML files.
  • HTML pages are more straightforward, clean and decoupled from the logic-based JavaScript.
  • Rather than cluttering your code by generating HTML using string concatenation, you use a lightweight template and interpolate JS variables.
  • Can be useful if you want dynamic content but don’t want to mess with server-side templating.
  • It can arguably minimize the amount of data that’s returned to the client, making sites faster and servers more responsive by lowering bandwidth and load.
  • Executes faster than server-side templating and provides an effortless way to create and maintain HTML templates.

You might want to adopt the technique of JS templating if you know your page will be updated frequently. Commons updates could include changes to the data either from the server via an API or data from the user. Also, if you're like me, you'll love how easy it is to manage your content. You'll no longer have to include important HTML markup in your JavaScript like this...

data.forEach (function (item) {
   html += '<li class="itemName">' + '' + item.name + ' -- Price: ' + item.price + '</li>'
})

Notice the intermingling of HTML and JavaScript is very tedious, difficult to follow and just plain sloppy.

Clean It up with JavaScript Templating

In short, JavaScript templating works by taking data that exists in your JS file and inserting that data into the appropriate positions within your HTML template.

You start by adding an HTML template to your project. This template will be parsed (analyzed and organized into logical syntactic components) from an HTML string to a DOM document.

In your JS file, you can specify functions, variables, objects, etc... that will be equal to whatever content you want displayed. Your HTML template will be interpolated with these values.

Ultimately, you end up with a string containing your JS values inserted into their correct positions, and then you insert that string as HTML on the page.

As you'll soon see in the example below, this technique keeps your HTML separate from your JavaScript, which allows you to comfortably manage your HTML and JS files. Let's take a closer look...

Easy as 1-2-3

There are 3 main steps involved in JavaScript Templating with Fly.

  1. Create an HTML template and include it in your .fly.yml file (which is basically your app's settings).
  2. Create an index.js file that contains variables, functions, objects, arrays, etc... that represent your content.
  3. Allow the two files to communicate through matching class names.

Below You'll See an Example Using JavaScript Templating with Fly in a Mad Libs Style Example.

# .fly.yml 
files: 
  - template.html 
<!-- template.html -->
<html>

<head>
  <title>Fly Mad Libs - JavaScript Templating</title>
  <style>
    label, [type=submit] { display: block; }
    hr { margin: 2em; }
  </style>
</head>

<body>
  <h1>Fly Mad Libs - Demonstrating JavaScript Templating with Fly</h1>
  <form>
    <label for="adjective">Think of an adjective...</label>
    <input type="text" name="adjective">
    <label for="verb">Think of a verb (present tense)...</label>
    <input type="text" name="verb">
    <label for="place">Think of a place...</label>
    <input type="text" name="place">
    <label for="person">Think of a person...</label>
    <input type="text" name="person">
    <label for="amtOfTime">Think of an amount of time...</label>
    <input type="text" name="amtOfTime">
    <label for="verb2">Think of another verb that ends in <i>-ing<i>...</label>
    <input type="text" name="verb2">
    <label for="noun">Think of a noun (plural)...</label>
    <input type="text" name="noun">
    <label for="adjective2">Think of an adjective ending in <i>-y</i>...</label>
    <input type="text" name="adjective2">
    <label for="noun2">Think of another noun (plural)...</label>
    <input type="text" name="noun2">
    <input type="submit" value="Create My Story">
  </form>
  <hr>
  <h2>The Story of Fly...</h2>
  <span class="one"></span>
  <b class="adjective"></b>.
  <span class="two"></span>
  <b class="verb"></b>
  <span class="three"></span>.
  <span class="four"></span>
  <b class="place"></b>.
  <span class="five"></span>
  <b class="person"></b>
  <span class="six"></span>.
  <span class="seven"></span>
  <b class="amtOfTime"></b>!
  <span class="eight"></span>.
  <span class="nine"></span>
  <b class="verb2"></b>
  <span class="ten"></span>.
  <span class="eleven"></span>
  <ul>
    <li>
      <span class="twelve"></span>
      <b class="noun"></b>
      <span class="thirteen"></span>.
    </li>
    <li>
      <span class="sixteen"></span>
    </li>
    <li>
      <span class="fourteen"></span>
      <b class="adjective2"></b>
      <span class="fifteen"></span>.
    </li>
    <li>
      <span class="seventeen"></span>
    </li>
  </ul>
  <span class="eighteen"></span>
  <b class="noun2"></b>,
  <span class="nineteen"></span>
</body>

</html>
// index.js
//respond to HTTP requests with HTML rendered from a template
fly.http.respondWith(async function (req) {
  const url = new URL(req.url)
  const template = await getTemplate()
  const doc = Document.parse(template)

  //some sentences to insert into the template
  const sentences = {
    one: "Fly is a global JavaScript runtime that makes your apps ",
    two: "With Fly, you can optimize images, pre-render, cache partials, ",
    three: " and more",
    four: "How does it work? Well, we run datacenters all over ",
    five: "When you deploy a Fly Edge Application, ",
    six: "is routed to the nearest datacenter",
    seven: "Our application servers connect them to your JavaScript environment in under ",
    eight: "Our open source Edge Application runtime works just like most modern application frameworks",
    nine: "Fly has tools for development + testing, and powerful APIs for ",
    ten: " CDN like applications",
    eleven: "With Fly you can:",
    twelve: "Fetch ",
    thirteen: "from anywhere and display them how you want",
    fourteen: "Prerender React apps for ",
    fifteen: " delivery",
    sixteen: "Use Fly's Image API to enable responsive images and deliver the exact images you need for each user without compromising load time.",
    seventeen: "Use fly.cache methods to rapidly deliver HTTP requests.",
    eighteen: "Are you ready for faster",
    nineteen: "simpler tools, and happier customers?"
  }

  // some words to insert into the template
  const words = {
    adjective: url.searchParams.get("adjective") || "____________",
    verb: url.searchParams.get("verb") || "____________",
    place: url.searchParams.get("place") || "____________",
    person: url.searchParams.get("person") || "____________",
    amtOfTime: url.searchParams.get("amtOfTime") || "____________",
    verb2: url.searchParams.get("verb2") || "____________",
    noun: url.searchParams.get("noun") || "____________",
    adjective2: url.searchParams.get("adjective2") || "____________",
    noun2: url.searchParams.get("noun2") || "____________"
  }

  // iterate over the words
  for (const v of Object.getOwnPropertyNames(words)) {
    // find elements to replace with var
    const className = `.${v}`
    const elements = doc.querySelectorAll(className)
    for (const el of elements) {

      // replace element with variable contents
      el.replaceWith(words[v]) // set value to innerHTML
    }
  }

  // iterate over the sentences
  for (const v of Object.getOwnPropertyNames(sentences)) {
    // find elements to replace with var
    const className = `.${v}`
    const elements = doc.querySelectorAll(className)
    for (const el of elements) {

      // replace element with variable contents
      el.replaceWith(sentences[v]) // set value to innerHTML
    }
  }

  // turn DOM back into html string
  const html = doc.documentElement.outerHTML // gets outerHTML of root element
  return new Response(html, { headers: { "Content-Type": "text/html" } })
})

//loads the contents of template.html
async function getTemplate() {
  const resp = await fetch("file://template.html")
  return await resp.text()
}

JS-templating

First and foremost, don’t forget to include your template in your .fly.yml file. If you forget this step, you will run into an error.

Our template.html file contains numerous spans with class names such as one, adjective, verb, noun, two etc... That's really all the significance that this file holds, class names. We'll see these come up again in our index.js file.

async function getTemplate() {  
    const resp = await fetch("file://template.html")  
    return await resp.text()  
} 

Our getTemplate() function fetches the bundled template.html and returns its text contents only (HTML markup will be removed).

const url = new URL(req.url)

Our url variable will be used to get the search params from the current window's URL. The URL interface can be used to parse URLs. The URLSearchParams interface can be used to build and manipulate the URL query string. So when the user submits our form, we'll grab their params from the URL.

const template = await getTemplate() 
const doc = Document.parse(template) 

Then we call our getTemplate() function and fetch our template. We use Document.parse on our template, which converts the raw markup into a Document object.

Our sentences and words objects contain the content that we will eventually insert into our HTML template. Notice the property names within these two objects exactly match the class names we set in our HTML template. This makes it possible for us to communicate between the two files and easily replace content.

Also, notice this line, adjective: url.searchParams.get("adjective") || "____________", within our words object. Here we're using our url variable, which we already know is equal to the current window's URL, and looking for a param in it that's equal to "adjective".

If there's no param matching "adjective", we will see a blank line, indicating that the user needs to fill in their answers.

for (const v of Object.getOwnPropertyNames(words)) {  
    const className = `.${v}`  
    const elements = doc.querySelectorAll(className)  
    for (const el of elements) {  
        el.replaceWith(words[v])   
    }  
} 

Object.getOwnPropertyNames(words) will return all properties of the object words. So v will be adjective, then verb, place, person, and so on. On each iteration, we'll be looking for elements to replace. If a class name from our template matches a property name from our object, the contents of the template will be replaced with the contents of the corresponding object's property. The replaceWith() method replaces selected elements with new content. Elements with matching classes get replaced entirely.

Then, the same is done for our sentences object.

const html = doc.documentElement.outerHTML 
Response(html, { headers: { "Content-Type": "text/html" } }) 

Finally, we're converting our Document object back into an HTML string and returning our template with all it's shiny, new content. Piece of cake, right?!

Where to Go from Here

Now that you know this isn't rocket science, you can easily start implementing this technique into your own apps. Here at Fly, we use these methods to deliver HTML templates with dynamic data. Dynamic templates are a splendid way to swap content in-and-out of your interface without adding a lot of extra markup and logic. Think of it as a little man that runs through your code, finds things that match between files, and replaces the old with the new. This little man seems very helpful if you ask me.

Sure, you could use a pure JavaScript templating system (like Handlebars.js) or use React to render server side and client side with the same code. Both work great in Fly apps!

But if your needs are simple and you don't want to over complicate things, this is a great, pain-free option. Happy templating!

Elise Barnes

@elise_barnes_

http://www.elisebarnes.org

Elise Barnes

Elise is a former lifestyle blogger, current Full Stack Web Developer/ Technical Content Creator. She loves JavaScript, learning new skills, and explaining challenging concepts to other developers.

New Jersey