Introduction
The objective of this project/prototype is to simplify the development of web applications (single-page applications) by reducing the inherent complexity. The guiding principle is the KISS ('Keep it simple, stupid') motto.
The Prototype II
This article follows the previous one I wrote and stems from refining the idea described there.
Now, let's consider a scenario where we want to display a list of contacts in a table. The common approach involves making a request to the server to obtain the list. However, various scenarios may unfold: one involves receiving a complete HTML page with the table already inserted (resulting in a reload); another entails receiving an HTML fragment with linked data that can be placed in the destination target (as exemplified, for instance, by the htmx library); and lastly, we may receive a JSON data structure in response, enabling the dynamic creation of the table through JavaScript on the client side.
The last scenario is one that many developers have encountered, considering the widespread use of the REST architecture and the multitude of APIs. These APIs exist due to their simplicity, scalability, and easy integration, serving as the glue that unites numerous services and supports various companies across SAAS (web development, e-commerce, service integration, microservices, cloud computing, internet of things, etc.). Hence, the concept of having a simple tool that almost magically generates the contact table on the client side using HTML templates, JavaScript Template literals, and a few more layers of magic is something that accelerates and streamlines the developer's workflow, saving a significant amount of time and effort.
The concept is not new and is already in use, involving the use of Template literals, formerly known as template strings. Template literals are enclosed with backtick ( `) characters, enabling multi-line strings, string interpolation with embedded expressions, and special constructs known as tagged templates.
let name = 'John Doe';
console.log(`Hello ${name}!`);
// returns Hello John Doe!
Template literals provide a concise and expressive way to create reusable components with HTML templates, simplifying variable insertion, supporting multiline strings, enabling expression evaluation, and enhancing overall code readability.
To implement this idea I created a library that is still a prototype that uses many of the ideas from the first article I wrote and some based on htmx and the knockoutjs library. This library is still in the early stages of development, so it is a prototype that evolves over time and is subject to structural changes and the addition of new ideas.
The idea I had is that, rather than selecting the template on the client side and defining the location where the template is placed along with the "transformations" to be applied, these same parameters can also be specified on the server side (but never both). This approach is practical because, depending on the data to be sent and the constraints encountered during processing, we can choose one template or another and determine different transformations for use on the client side.
To associate the JSON data structure with the template, the web server sends a personalized header (Automata-Transformation) along with the body. This header includes information about the transformation to be applied to the template on the client side and specifies the final destination for the resulting transformation outcome. The header consists of its case-insensitive name followed by a colon (:), then by its value (key/value).
Response Header Example:
HTTP/1.1 200 OK
Automata-Transformation: target:#example;template:#example-tpl;swap:afterbegin;remove:#example-modal
Content-Type: application/json
{
"employee": {
"name": "John",
"age": 30,
"city": "New York"
}
}
List of available properties to use in the HTTP header or tasks:
- target: uses the first document element that matches the specified CSS selector as the final destination for data association with the template;
- template: this property enables you to choose the template for use to associate the data. If the name starts with the character "#", it indicates that the template is embedded in the destination page and corresponds to the "id" of the element to be utilized. Conversely, if it starts with "@", it signifies that it is a template to be loaded from the templates directory;
- swap: this property controls how content is swapped into the target element (default: innerHTML) (optional);
- remove: uses a list of document elements matching the specified group of CSS selectors to be removed before inserting the template into the destination (optional).
While the transformation can be defined on the client side, the server-side HTTP header is optional. However, it proves practical on the server side, allowing you to decide which template to use based on the data.
When the response reaches the client, the matching is done with the selected template using template literals. Each literal expression is converted into a string corresponding to the content of the sent data structure. The variable with the JSON structure that contains the data is always called "data".
Experiment
To better visualize the process, let's create a table with a list of contacts retrieved from a JSON response. To achieve this, we'll first construct a JSON data structure that represents the contacts, as shown below.
[
{
"id": "1",
"name": "Lorem Ipsum",
"email": "lorem.ipsum@example.com",
"status": "Active"
},
{
"id": "2",
"name": "Mauris Quis",
"email": "mauris.quis@example.com",
"status": "Active"
},
{
"id": "3",
"name": "Donec Purus",
"email": "donec.purus@example.com",
"status": "Active"
}
]
Now that we have the list, let's create a HTML template and associate it with the contact data using the Template literals mentioned above. HTML templates can be embedded (identified by the HTML element "template" or loaded from a URL. This latter option is ideal for creating reusable components that can be inserted into the HTML structure.
Template example:
<template id="contacts-list-tpl">
<textarea data-codeblock>
<table>
<thead>
<tr>
${(function () {
let headers = [];
for (const header of [" ", "Name", "Email", "Status"]) {
headers.push(`<th>${header}</th>`);
}
return headers.join("");
}())}
</tr>
</thead>
<tbody>
${data.map(record => `<tr>
<td>
<input type="checkbox"
name="id"
value="${record.id}">
</td>
<td>${record.name}</td>
<td>${record.email}</td>
<td>${record.status}</td>
</tr>`).join("")}
</tbody>
<tfoot>
<tr>
<td colspan="4">
<button name="switch-status" value="activate"
data-tasks="switch-contact-status" disabled>Activate</button>
<button name="switch-status" value="deactivate"
data-tasks="switch-contact-status" disabled>Deactivate</button>
</td>
</tr>
</tfoot>
</table>
</textarea>
</template>
We have a template with a table for the contact list. If we inspect the table, we notice that the template element is followed by a "textarea" element. This arrangement allows the HTML code inside the element to be preserved as is for interpolation. If the "textarea" element is omitted, all content within the template is converted to plain text, rendering the template unusable. This method works effectively as long as we ensure that all expressions within the backticks resolve to strings.
In the template, there are two buttons for activation/deactivation. Each button has a "data-tasks" attribute, which is utilized to execute an action based on the rules defined in a tasks file (refer to the previous article).
Expected result after data binding:
<table>
<thead>
<tr>
<th> </th>
<th>Name</th>
<th>Email</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="checkbox" name="id" value="1"></td>
<td>Lorem Ipsum</td>
<td>lorem.ipsum@example.com</td>
<td>Active</td>
</tr>
<tr>
<td><input type="checkbox" name="id" value="2"></td>
<td>Mauris Quis</td>
<td>mauris.quis@example.com</td>
<td>Active</td>
</tr>
<tr>
<td><input type="checkbox" name="id" value="3"></td>
<td>Donec Purus</td>
<td>donec.purus@example.com</td>
<td>Active</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="4">
<button name="switch-status" value="activate"
data-tasks="switch-contact-status">Activate</button>
<button name="switch-status" value="deactivate"
data-tasks="switch-contact-status">Deactivate</button>
</td>
</tr>
</tfoot>
</table>
We have also created a "tasks.json" file with the list of tasks defined in buttons with the "data-tasks" attribute, as explained in the previous article.
{
"get-contacts-list": {
"action": "/listcontacts",
"method": "get",
"trigger": "click"
},
"switch-contact-status": {
"action": "/statuscontact",
"method": "put",
"collect-data": "#contacts-list > table > tbody > tr > td > input[name]",
"trigger": "click"
}
}
Available properties for each task:
- action: the action that is executed on the server when the event is triggered;
- attribute-action: replaces the "action" property with the specified attribute, which should be present on the element with the 'data-tasks' attribute (optional);
- method: set a request method (GET, POST, PUT, PATCH or DELETE) to indicate the desired action to be performed for a given resource;
- collect-data: uses the data attributes given by the first element within the document that matches the specified selector as a source of data (optional);
- trigger: specifies the event that triggers the request;
We can also utilize the properties available for the HTTP header (Automata-Transformation) here, but if the header already exists, the properties specified there take priority.
Finally, we will create an HTML page (index.html) containing the template and a field that serves as the target for the template after data association. Within this field, there is a button to trigger the association.
The JavaScript library ("Automata.js"), responsible for processing events and binding data to the attributes, was added at the end.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Test</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Cache-control" content="no-cache">
<meta name="author" content="Henrique Dias">
<link rel="stylesheet" href="styles.css?2024011101">
</head>
<body>
<div id="contacts-list">
<button data-tasks="get-contacts-list"
class="contact-button">
Get Contacts List
</button>
</div>
<!-- embedded templates -->
<template id="contacts-list-tpl">
<textarea data-codeblock>
<table>
<thead>
<tr>
${(function () {
let headers = [];
for (const header of [
" ", "Name",
"Email", "Status"]) {
headers.push(`<th>${header}</th>`);
}
return headers.join("");
}())}
</tr>
</thead>
<tbody>
${data.map(record => `<tr>
<td>
<input type="checkbox"
name="id" value="${record.id}">
</td>
<td>${record.name}</td>
<td>${record.email}</td>
<td>${record.status}</td>
</tr>`).join("")}
</tbody>
<tfoot>
<tr>
<td colspan="4">
<button name="switch-status"
value="activate"
data-tasks="switch-contact-status"
disabled>Activate</button>
<button name="switch-status"
value="deactivate"
data-tasks="switch-contact-status"
disabled>Deactivate</button>
</td>
</tr>
</tfoot>
</table>
</textarea>
</template>
<script>
/* <![CDATA[ */
import("./Automata.js?2024011001").then((module) => {
const automata = new module.Automata();
automata.init();
});
/* ]]> */
</script>
</body>
</html>
Now that the frontend is planned, let's shift our focus to the backend. We'll create a web server in Go (can be in any language or framework) to handle incoming requests. When you click on the "Get contact list" button, a request is sent to the server to retrieve the contact list. Assuming the contacts are stored in a database, the server queries them and generates a JSON data structure containing the selected list of contacts, as shown above.
In the response to be sent to the client, some magic is included — a new custom HTTP header (Automata-Transformation) is added, containing information about the transformation to be applied. An example is provided below.
HTTP/1.1 200 OK
Automata-Transformation: target:#contacts-list;template:#contacts-list-tpl;swap:innerHTML
Content-Type: application/json
The web server code (server.go) is basic and intended solely for demonstration purposes. Static content is served from the "public" folder. Click here to see the python version.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os/exec"
"runtime"
"strings"
"time"
)
type Contact struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Status string `json:"status"`
}
var contacts = []Contact{
{
Id: "1",
Name: "Lorem Ipsum",
Email: "lorem.ipsum@example.com",
Status: "Active",
},
{
Id: "2",
Name: "Mauris Quis",
Email: "mauris.quis@example.com",
Status: "Active",
},
{
Id: "3",
Name: "Donec Purus",
Email: "donec.purus@example.com",
Status: "Active",
},
}
func openUrlInBrowser(url string) error {
var cmd string
var args []string
switch runtime.GOOS {
case "windows":
cmd = "cmd"
args = []string{"/c", "start"}
case "darwin":
cmd = "open"
default: // "linux", "freebsd", "openbsd", "netbsd"
cmd = "xdg-open"
}
args = append(args, url)
return exec.Command(cmd, args...).Start()
}
func transformationHeader(transformations map[string]string) string {
return strings.Join(func() []string {
list := []string{}
for k, v := range transformations {
list = append(list, fmt.Sprintf("%s:%s", k, v))
}
return list
}(), ";")
}
func switchContactStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPut {
http.Error(w, fmt.Sprintf(`{"message": "Method %s Not Allowed"}`, r.Method),
http.StatusMethodNotAllowed)
return
}
w.Header().Set("Automata-Transformation",
transformationHeader(map[string]string{
"target": "#contacts-list",
"template": "@contacts-list",
}))
content := struct {
SwitchStatus string `json:"switch-status"`
Id []string `json:"id"`
}{}
if err := json.NewDecoder(r.Body).Decode(&content); err != nil {
http.Error(w, fmt.Sprintf(`{"message": "%s"}`, err.Error()),
http.StatusBadRequest)
return
}
for _, id := range content.Id {
for i := range contacts {
if contacts[i].Id == id {
contacts[i].Status = func() string {
if strings.EqualFold(content.SwitchStatus, "deactivate") {
return "Inactive"
}
return "Active"
}()
}
}
}
jsonResp, err := json.Marshal(contacts)
if err != nil {
http.Error(w, fmt.Sprintf(`{"message": "%s"}`, err.Error()),
http.StatusInternalServerError)
return
}
w.Write([]byte(jsonResp))
}
func listContact(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodGet {
http.Error(w, fmt.Sprintf(`{"message": "Method %s Not Allowed"}`, r.Method),
http.StatusMethodNotAllowed)
return
}
w.Header().Set("Automata-Transformation",
transformationHeader(map[string]string{
"target": "#contacts-list",
"template": "#contacts-list-tpl",
"swap": "innerHTML",
}))
jsonResp, err := json.Marshal(contacts)
if err != nil {
log.Fatalf("Err: %v\r\n", err)
}
w.Write([]byte(jsonResp))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/statuscontact", switchContactStatus)
mux.HandleFunc("/listcontacts", listContact)
mux.Handle("/", http.FileServer(http.Dir("public/")))
go func() {
<-time.After(100 * time.Millisecond)
openUrlInBrowser("http://localhost:8080")
}()
server := http.Server{
Addr: ":8080",
Handler: mux,
}
fmt.Printf("Server listening on %s", server.Addr)
if err := server.ListenAndServe(); err != nil {
panic(err)
}
}
If we examine the code, we can see in the "switchContactStatus" function that, in the custom HTTP header, the template is fetched from a URL (its name starting with the "@" character) instead of using a template embedded in the HTML page as in the "listContact" function. All templates are located in the "templates" directory.
The following directory structure is just a suggestion and can be adjusted to the needs of each project.
-
list-contacts/
- public/
- Automata.js
- index.html
- styles.css
- tasks.json
- templates/
- server.go
- server.py (alternative)
- public/
Now that the code is ready and saved in the root of the directory structure indicated above, let's run the server with the following command:
$ go run .
Server listening on :8080
Due to the "openUrlInBrowser" function, the web page will automatically open in your default browser. If it doesn't work, you can manually enter the address http://localhost:8080 to access it. Once on the page, you should see a button with the text "Get Contact List".
Conclusion
Associating JSON data structures with HTML templates using JavaScript Template literals is a practical way to abstract ourselves from the complexity of generating HTML on the client side with JavaScript alone. Template literals simplify the process of directly inserting variables into the template, making it easier to create and maintain HTML templates for reusable components. While not all use cases may currently be covered by this strategy, it contributes to accelerating the development speed.
The source code for this prototype is available here: list-contacts
Thank you for reading!
Top comments (0)