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 [Part 1],
- Javascript rendering [Part 2],
- Hotwire [This part],
- Conceptual differences of all approaches and how to choose [Part 4]
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.
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.
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.
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