[Part 1] Hotwire - between the HTML (server side) and the Javascript rendering. What to choose?

The user interfaces in web applications have been implemented using different methodologies over time. First, we have had classic HTML rendering. After that, we started to utilize Javascript and partial HTML views. The latest point was the utilization of javascript frameworks for complete interface renderings like React, Angular, Vue, and Ember. Hotwire turbo fits somewhere in between HTML and Javascript rendering. It turns out that there are business problem categories where each of the three approaches is preferable over the others. This article's focus is to describe these different approaches and tackle decision-making process based on the given business problem.

[Part 1] Hotwire - between the HTML (server side) and the Javascript rendering. What to choose?

Article parts

To tackle the topic of web application interface (GUI) rendering and to compare available approaches, the article is split into following parts:

  • HTML and HTML partials rendering [This part],
  • Javascript rendering [Part 2],
  • Hotwire [Part 3],
  • Conceptual differences of all approaches and how to choose the most applicable one [Part 4].

GitHub Repository

Complete implementation of the solution shown in this article can be cloned from GitHub repository: https://github.com/emir-gradient/express-ejs-rendering.

It all started with "HTML rendering"

Let's start by defining starting point in history for this article. So, the first point we analyze this from is the point in time when browsers supported HTML, CSS, and ES5 Javascript. At that moment in time, React, Angular, Vue, Ember, Backbone, etc. are not invented yet. Frameworks to support web application development by HTML rendering exist. There are Express.js, Laravel, Yii, CodeIgniter, Ruby on Rails, Django, Java Server Pages, Java Server Faces, Asp, etc.

When it comes to HTML rendering, the approach is to react to the user's interaction using the server to provide every next page (view) in the HTML format. We can state that by this approach server builds the resulting HTML.

In the more detail, the approach is as follows:

  • The user interacts with the server using the browser. They do so either by typing the initial page URL or clicking a link/button on the currently rendered interface.
  • The browser creates and sends HTTP requests to the server.
  • The server processes HTTP request and returns response in the HTML format back to the browser.
  • The browser renders the received HTML on the page.
  • The browser requests and receives all necessary CSS and Javascript files (based on the content in retrieved HTML).
  • The browser executes received CSS and Javascript files (meaning that complete user interface is presented).

Following is the example project that is developed and suitable for HTML rendering.

Perhaps one of the simpler ways to present any web application server concept is to utilize Express.js from the listed frameworks. The rest of the listed frameworks follow the same principle for HTML rendering, but on a higher scale. These are providing us with additional utility functionalities that are commonly needed when building web applications. Some Node frameworks are actually built as additional layer on Express.js.

Express.js provides us with solid ground to build complete web applications with all the listed additional functionalities. In comparison with the rest of the listed frameworks, Express.js is unopinionated about how web application implementation should be built and structured, so we as developers need to include necessary additional functionalities, either by developing them or utilizing other third-party libraries to support our goal. Building production-ready web application example in Express.js is out of the scope of this article.

To cover HTML rendering concepts, it is good to have a verbose framework, and not one that performs common concepts "behind the curtain". Hence, Express.js with a minimum of additional libraries is utilized.

Basic web server

A basic web server implementation in Express.js that responds to a user's request on http://localhost:3000 is implemented as follows:

src/index.js

const express = require('express');

const app = express();
const port = 3000;

app.get('/', (req, res) => {
  res.send(
`<!DOCTYPE html>
<html lang="en-US">
<head>
    <title>Example Project</title>
</head>
<body>
    <h1>Example Project Body</h1>
</body>
</html>
`
);
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

On every user's request for the '/' path, static HTML is returned and rendered in the browser. This is not a very interesting application and can not (for now) be named an application. The applicable name is a "static site".

Blog picture

Dynamic greeting application

To make one dynamic page, we decide on inputs and use the req parameter (Request) in our handler functions (req is the object that contains data mapped from the user's HTTP request).

The following code section greets the user based on its name sent as a query parameter:

src/index.js

...

app.get('/greet', (req, res) => {
  // Processing request data part
  const name = req.query.name ? req.query.name : null;
  const message = name ? `Hello ${name}!` : `Hello!`;

  // Rendering part
  res.send(
`<!DOCTYPE html>
<html lang="en-US">
<head>
    <title>Example Project</title>
</head>
<body>
    <h1>${message}</h1>
</body>
</html>
`
);
});

...

The /greet page is not static. Its content depends on the input provided in the HTTP request. In this example, one can recognize that the processing of input data and rendering are separated. The greeting message is changed depending on the existence and content of the query parameter name in the request. When the final message is ready then it is used in the rendering phase.

Blog pictureBlog picture

Separate rendering from processing

To achieve more practical separation of processing from rendering in the server implementation, we usually include a template engine. In this article, we are utilizing ejs (Embedded Javascript) as a template engine. It allows us to organize our configurable HTML sections (templates) into different files, parameterize them, and write javascript inside the templates when we need dynamic behavior. After restructuring and utilization of ejs, the content of our files is:

src/index.js

const express = require('express');

const app = express();
const port = 3000;

// Define for Express.js that 'ejs' view engine is utilized
app.set('view engine', 'ejs');

app.get('/', (req, res) => {
  // Rendering
  res.render('home');
});

app.get('/greet', (req, res) => {
  // Processing
  const name = req.query.name ? req.query.name : null;
  const message = name ? `Hello ${name}!` : `Hello!`;

  // Rendering
  res.render('greet', {
    message
  });
});

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`);
});

With ejs included, HTML is moved into home.ejs and greet.ejs view files. Common sections of these two pages are moved to partials/header.ejs and partials/footer.ejs files. The resulting content is as follows:

views/home.ejs

<%- include('partials/header', { title: 'Example Project' }); %>

<h1>Example Project Body</h1>

<%- include('partials/footer'); %>

views/greet.ejs

<%- include('partials/header', { title: 'Greeting Example' }); %>

<h1><%= message %></h1>

<%- include('partials/footer'); %>

views/partials/header.ejs

<!DOCTYPE html>
<html lang="en-US">
<head>
    <title><%= title %></title>
</head>
<body>

views/partials/footer.ejs

</body>
</html>

When rendering is performed, a single HTML response is returned from the server and rendered in the browser.

This means that Express takes care of processing HTTP request/response and ejs takes care of filling in provided arguments (like message and title), combining views and partials (processing include instruction), and preparing final HTML content as the result.

Accept the user's input

To take the user's input, server renders the HTML form that the user should fill in and then make an HTTP request with filled data. To achieve that, in index.js we define a handler that is supposed to render input form as follows:

src/index.js

...

app.get('/add-name', (req, res) => {
  res.render('add-name');
});

...

and proper view:

src/add-name.ejs

<%- include('partials/header', { title: 'Example Project' }); %>

<h1>Add your name:</h1>
<form method="get" action="/greet">
    <label for="name">Name:</label>
    <input id="name" type="text" name="name">

    <button type="submit">Submit</button>
</form>

<%- include('partials/footer'); %>

With this implementation, when the user accesses /add-name route, it is presented with the HTML form to fill in its name. By clicking Submit button, GET HTTP request to /greet route is sent with name in query parameters.

Blog pictureBlog picture

Navigation

To allow users to navigate between pages without using the browser address bar to actually type URLs, we can build a simple Navigation Bar as follows:

partials/navbar.ejs

<% for (const navItem of navItems) { %>
    <a href="<%= navItem.path %>"><%= navItem.title %></a> |
<% } %>

To make sure the navbar is visible on the bottom of every page, we can include it in the footer right before the closing </body> tag.

partials/footer.ejs

<%- include('navbar', { navItems: [
    {
       path: '/',
       title: 'Home'
    },
    {
       path: '/greet',
       title: 'Default Greet'
    },
    {
       path: '/add-name',
       title: 'Add Name'
    }
] }); %>

</body>
</html>

After these changes, users can navigate between available pages in the web application.

Blog pictureBlog pictureBlog pictureBlog picture

CSS / Styling

When we do HTML rendering we provide CSS as a separate asset, providing it from the same server that serves the application, or an independent external server (like CDN).

To provide it from the Express server in the example application, we define a directory that is statically served as follows:

src/index.js

...

app.set('view engine', 'ejs');

// Add this line
app.use(express.static('public'));

...

After that we place our stylesheets in the public directory like in the following example:

public/example-application.css

body {
    padding: 50px;
    background: linear-gradient(to bottom right, #009ea7, #009ed7) fixed;
}

h1 {
    color: yellow;
    text-shadow: pink 2px 1px;
}

a {
    color: white;
    font-weight: bold;
}

form {
    height: 50px;
}

label {
    color: yellow;
}

input {
    border-radius: 10px;
    height: 25px;
}

button {
    height: 30px;
    border-radius: 10px;
}

and finally, we put the stylesheet link in header.ejs partial, so it can be requested by the browser once it displays an HTML response from the server:

views/partials/header.ejs

...

<title><%= title %></title>
<link rel="stylesheet" href="/example-application.css">

...
Blog pictureBlog pictureBlog pictureBlog picture

HTML Partials Rendering

After HTML rendering, the next used approach is HTML partials rendering. By this design, the goal is to achieve that the server processes only part of the page that is affected by the user's interaction. So, instead of responding to every request with full-page content, the server responds only with relevant HTML Partial.

This functionality is achieved using Javascript.

To cover the idea behind this approach we proceed with the existing web application example. When we use our example application, we can recognize that our footer links list never changes. It stays fixed during the complete application usage. So, with this approach, we will make sure to have Javascript that would perform HTTP requests for us (instead of using native HTML behavior with links and forms) and place response in the "Main Area" of the HTML page.

Additionally, our server will be capable of returning just parts of HTML related to the requested operation (HTML partials).

So, staring with index.js by defining a request to /partial-home that will return only the home page section (without header and footer):

src/index.js

...

app.get('/partial-home', (req, res) => {
  res.render('partials/_home');
});

...

Following it with view changes by creating views/partials/_home.ejs and reusing it in views/home.ejs (one dedicated for a full page response).

views/partials/_home.ejs

<h1>Example Project Body</h1>

views/home.ejs

<%- include('partials/header', { title: 'Example Project' }); %>

<div id="main-area">
    <%- include('partials/_home'); %>
</div>

<%- include('partials/footer'); %>

Note that we added div with id main-area around the _home partial. This id is relevant for javascript implementation to define the place where server partial HTML responses need to be put.

After these changes, if we access the http://localhost:3000/partial-home endpoint, we get the response that only contains "Example Project Body" content.

No styles, and no navigation, since only partial is rendered.

The goal is to have javascript that will fill in the main-area div with partial results whenever the navigation item is clicked.

To achieve this, first, we prepare all partial endpoints and then we change views/partials/footer.ejs file content to target the "partial" endpoints. That implementation looks as follows:

src/index.js

...

app.get('/partial-home', (req, res) => {
  res.render('partials/_home');
});

app.get('/partial-greet', (req, res) => {
  const name = req.query.name ? req.query.name : null;
  const message = name ? `Hello ${name}!` : `Hello!`;

  res.render('partials/_greet', {
    message
  });
});

app.get('/partial-add-name', (req, res) => {
  res.render('partials/_add-name');
});

...

views/partials/_greet.ejs

<h1><%= message %></h1>

views/partials/_add-name.ejs

<h1>Add your name:</h1>
<form method="get" action="/partial-greet">
    <label for="name">Name:</label>
    <input id="name" type="text" name="name">

    <button type="submit">Submit</button>
</form>

views/greet.ejs

<%- include('partials/header', { title: 'Greeting Example' }); %>

<div id="main-area">
    <%- include('partials/_greet'); %>
</div>

<%- include('partials/footer'); %>

views/add-name.ejs

<%- include('partials/header', { title: 'Example Project' }); %>

<div id="main-area">
    <%- include('partials/_home'); %>
</div>

<%- include('partials/footer'); %>

views/partials/footer.ejs

<%- include('navbar', { navItems: [
    {
       path: '/partial-home',
       title: 'Home'
    },
    {
       path: '/partial-greet',
       title: 'Default Greet'
    },
    {
       path: '/partial-add-name',
       title: 'Add Name'
    }
] }); %>

</body>
</html>

With these changes, it is achieved that every endpoint that returned a full HTML page still returns the full pages, so when the user arrives at any of the original URLs, it gets the whole page loaded. Links in navigation, though, now target only partial HTML rendering endpoints.

Now, we want to achieve that clicking on every one of these links (and submitting every HTML form) triggers request for appropriate partial HTML endpoint. What we do not want is for the browser to refresh the page with new HTML result, as it does by default behavior.

So, the following implementation will override the default browser behavior on user's interaction. Goal is to make a request, take the HTML response, and place it in the main-area div.

The following Javascript solution achieves that:

public/example-application.js

// Execute this function when the page is loaded in the browser.
window.onload = function () {
  const navigationLinks = document.getElementsByTagName("a");
  const navigationLinksCount = navigationLinks.length;

  for (let i = 0; i < navigationLinksCount; i++) {
    const navLink = navigationLinks[i];
    
    // Behavior that needs to be performed every time navigation link is clicked.
    navLink.onclick = function (event) {
      const url = event.target.attributes['href'].nodeValue;
      makeRequestAndUpdate(url);

      event.preventDefault();
    }
  }

  overrideFormSubmit();
}

// Functionality that overrides default browser "Form Submit" behavior.
function overrideFormSubmit() {
  const forms = document.getElementsByTagName("form");
  const formsCount = forms.length;

  for (let i = 0; i < formsCount; i++) {
    const formElement = forms[i];
    formElement.onsubmit = function (event) {
      const url = event.target.attributes['action'].nodeValue;

      const fd = new FormData(event.target);
      const queryParams = new URLSearchParams(fd).toString();

      makeRequestAndUpdate(`${url}?${queryParams}`);

      event.preventDefault();
    };
  }
}

// Executing actual HTTP request to the server and placing the response into 'main-area' div.
function makeRequestAndUpdate(url) {
  const xhr = new XMLHttpRequest();

  xhr.open("GET", url, true);

  xhr.onreadystatechange = function () {
    // readyState being 4 means that request processing is "Done".
    // status being 200 means that http request processing was "successful" by the server.
    if (this.readyState == 4 && this.status == 200) {
      const partialHtmlResponse = this.responseText;

      document.getElementById('main-area').innerHTML = partialHtmlResponse;
      overrideFormSubmit();
    }
  }

  xhr.send();
}

and including it in the footer as follows:

views/partials/footer.ejs

...

<script src="/example-application.js"></script>

</body>
</html>

With these steps, our application renders only partials on every link click and form submit.

Note: Javascript presented in this example is made for this example application, and needs to be significantly adapted for general usage.

In the following screenshot, one can see all http requests sent from our application utilizing 'Network tab'. One can see individual requests made for partials.

Blog picture

Get an Offer

Contact Us or Schedule a Meeting with us to get an offer for our development & consulting services regarding your current or next Web project.

There are no comments

Leave your comment