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

After the analysis of HTML (server side) rendering in the [Part 1], and Javascript (client side) rendering in the [Part 2], we are focusing on one "hybrid" idea in this article. In this approach, we are utilizing Hotwire Turbo to take care of GUI rendering. We do utilize minimal Javascript in this approach, and partials rendering is still on the server side. Hotwire Turbo provides us with "all in one" Javascript bundle that autonomously communicates with server and updates GUI as required.

[Part 3] 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:

GitHub Repository

There is one solution presented in this article and complete implementation of solution shown in this article can be cloned from GitHub repository: https://github.com/Gradient-s-p/express-hotwire-rendering.

Hotwire Turbo

Hotwire Turbo is one toolkit provided to us by 37signals. About all provided Hotwire toolkits, one can read here.

Turbo module is installed as external Javascript reference in HTML of the Web application. This module intercepts http requests that would be by default performed in browser (hyperlink clicks and form submits) and performs Web Socket communication with the server. Also, only parts of the interface (controlled by server) are delivered to client and replaced by Turbo.

Full idea is to have hybrid behavior that semantically fits between server-side HTML rendering and client-side Javascript rendering in a way that initial page load is delivered as final HTML, but every change/interaction on the interface is done with Javascript.

More on the positive and negative sides of these approaches will be written in [Part 4] of this article.

Greeting App

Throughout all three parts of this article we are implementing the same Greeting App utilizing different technologies and rendering methods. This part is not exception.

If we check the result of [Part 1] of this article, there is Greeting App with partials rendering developed (and can be cloned from GitHub repository shared in that article). In the [Part 1], we have incorporated our own relatively small and relatively simple JavaScript implementation that intercepts link clicks and form submits. Mentioned implementation was written for the Greeting App explicitly, and will not work correctly for general usage on other applications.

For the purpose of Hotwire Turbo integration, we will start with that application, and perform step by step process to replace that Javascript partials rendering solution with Hotwire Turbo.

Removing the JavaScript partials replacement solution

File public/example-application.js consists of our partials replacement solution. So, we start by removing it from the source code.

This JavaScript solution is also referenced from views/partials/footer.ejs view file. We remove that reference also. After that, footer.ejs file looks as follows:

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>

If we, after these removals, start our server we see that home page works correctly, but partials are rendered as new pages (instead of being replaced into 'main-area'). JavaScript module that we removed has been utilized for that.

Blog pictureBlog picture

Including Hotwire Turbo

There are multiple ways to integrate Hotwire Turbo to our web application. In this example, we use installation from npm registry.

We install "@hotwired/turbo" as a dependency using following command in the root of the project:

yarn add @hotwired/turbo

After successful installation, client script is saved in node_modules/@hotwired/turbo/dist/turbo.es2017-umd.js. There are multiple viable approaches to deliver this script from the server. In this article, approach where server statically serves turbo.es2017-umd.js directly from node_modules is utilized.

So, we extend our server implementation with the following changes:

src/index.js

const express = require('express');
const path = require('path');

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

app.set('view engine', 'ejs');
app.use(express.static('public'));

app.get('/turbo.es2017-umd.js', (req, res) => {
  const turboLibPath = path.join(process.cwd(), "node_modules/@hotwired/turbo/dist/turbo.es2017-umd.js");
  res.sendFile(turboLibPath);
});

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

// Rest of the implementation

views/partials/header.ejs

<!DOCTYPE html>
<html lang="en-US">
<head>
    <title><%= title %></title>
    <link rel="stylesheet" href="/example-application.css">

    <script src="/turbo.es2017-umd.js"></script>
</head>
<body>

After starting our server, we can notice that home page looks exactly the same. But when we click on any of navigation links, styles are preserved because complete HTML <body> is replaced, but not style part. Also, if we analyze through Network Tab in browser (Inspect/Developer Console) we can notice that http requests towards server are XHR ones initiated from turbo.es2917-umd.js, meaning that all link clicks are intercepted by Turbo implementation that we just included.

Blog pictureBlog picture

Optimizing Responses by Including Only Necessary Partials

Currently, our system renders the entire page on every HTTP request, a legacy from the earlier Express version built in [Part 2]. Our goal is to replace only the main area of the application. To begin, we need to define the main area.

views/home.ejs

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

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

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

views/partials/_home.ejs

<turbo-stream action="update" target="main-area">
    <template>
            <h1>Example Project Body</h1>
    </template>
</turbo-stream>

Let's explain the components of views/partials/_home.ejs part by part:

  • <turbo-stream /> tag – This tag in the response informs the underlying TurboStream JavaScript implementation that this statement is meant for it.
  • action="update" – This attribute tells TurboStream to perform an update.
  • target="main-area" – This attribute specifies which element on the interface should be replaced.
  • <template /> tag – This tag defines the template containing the content that will be used for the replacement.

This response instructs: "Replace the content of the element with the id 'main-area' with the following provided template."

We apply the same incorporation with the other templates.

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/_add-name'); %>
</div>

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

views/partials/_greet.ejs

<turbo-stream action="update" target="main-area">
    <template>
        <h1><%= message %></h1>
    </template>
</turbo-stream>

views/partials/_add-name.ejs

<turbo-stream action="update" target="main-area">
    <template>
        <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>
    </template>
</turbo-stream>

And finally, server related to delivering main (initial) pages for three endpoints looks as follows:

src/index.js

// Rest of the implementation

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

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

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

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

// Rest of the implementation

With partials prepared using TurboStream instructions, the only remaining requirement is for the server to respond with the appropriate partials in the correct HTTP content type. The implementation is as follows.

src/index.js

// Rest of the implementation

app.get('/partial-home', (req, res) => {
  res.contentType('text/vnd.turbo-stream.html');
  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.setHeader('Content-Type', ['text/vnd.turbo-stream.html']);
  res.render('partials/_greet', {
    message
  });
});

app.get('/partial-add-name', (req, res) => {
  res.contentType('text/vnd.turbo-stream.html');
  res.render('partials/_add-name');
});

// Rest of the implementation

With this step, Greeting App is completely incorporated using Hotwire.

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