Today, we often reach for complex frameworks and toolchains to create interactive forms—but what if we told you that you can build smart, dynamic forms without writing a single line of traditional JavaScript logic?
In this article, we’ll show you how to create a fully functioning form that submits asynchronously using HMPL, a lightweight templating engine that simplifies client-server interactions.
Let’s start!
🗄️ Project Structure
We’ll use a simple folder layout:
📁 smart-form
├── 📁 components
│ └── 📁 Register
│ └── index.html
├── 📁 src
│ ├── global.css
│ ├── global.js
│ └── index.html
├── app.js
└── routes
└── post.js
- Server: Pure HTML, CSS, and HMPL templates.
- Client: Node.js + Express to receive form data.
No frameworks like React, Vue, or even jQuery. Just clean web APIs and declarative logic.
🖋️ Styling the Form
Let’s start with our basic styles.
src/global.css
body {
font-family: Arial, sans-serif;
background: #f4f4f4;
padding: 50px;
}
form {
background: white;
padding: 20px;
border-radius: 6px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
max-width: 400px;
margin: auto;
}
.form-example {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
input[type="submit"] {
background-color: #649606;
color: white;
border-radius: 5px;
padding: 10px 15px;
cursor: pointer;
}
📡 Creating the Server
We’ll set up a simple Express server with one POST route to handle our form submission.
app.js
const express = require("express");
const path = require("path");
const cors = require("cors");
const app = express();
const PORT = 8000;
app.use(cors());
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(express.static(path.join(__dirname, "src")));
const postRoutes = require("./routes/post");
app.use("/api", postRoutes);
app.get("/", (_, res) => {
res.sendFile(path.join(__dirname, "src/index.html"));
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
routes/post.js
const express = require("express");
const router = express.Router();
router.post("/register", (req, res) => {
const { login, password } = req.body;
if (!login || !password) {
return res.status(400).send("<p style='color: red;'>Missing fields!</p>");
}
console.log("User Registered:", login);
res.send(`<p style='color: green;'>Welcome, ${login}!</p>`);
});
module.exports = router;
🧠 The Smart Form Component
Here’s where the magic happens. This form will submit data using HMPL's request
block, without you writing any JavaScript event listeners.
components/Register/index.html
<div>
<form onsubmit="function prevent(e){e.preventDefault();};return prevent(event);" id="form">
<div class="form-example">
<label for="login">Login:</label>
<input type="text" name="login" id="login" required />
<br/>
<label for="password">Password:</label>
<input type="password" name="password" id="password" required />
</div>
<div class="form-example">
<input type="submit" value="Register!" />
</div>
</form>
<p>
{{#request
src="/api/register"
after="submit:#form"
repeat=false
indicators=[
{
trigger: "pending",
content: "<p>Loading...</p>"
}
]
}}
{{/request}}
</p>
</div>
What’s happening here?
-
onsubmit
prevents default behavior. -
{{#request}}
captures the form submit event. -
after="submit:#form"
defines when the request should fire. -
indicators
show loading states or feedback.
No manual fetch
, no async/await. Everything is declared.
⚙️ Loading the Component with HMPL
Now, let’s render this component dynamically on our page using HMPL.
src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Smart Form</title>
<link rel="stylesheet" href="global.css" />
</head>
<body>
<div id="wrapper"></div>
<script src="https://unpkg.com/json5/dist/index.min.js"></script>
<script src="https://unpkg.com/dompurify/dist/purify.min.js"></script>
<script src="https://unpkg.com/hmpl-js/dist/hmpl.min.js"></script>
<script>
import { compile } from "hmpl-js";
const templateFn = compile(`
{{#request src="/components/Register/index.html"}}{{/request}}
`);
const obj = templateFn();
document.getElementById("wrapper").append(obj.response);
</script>
</body>
</html>
Optionally, you can break this logic out into a separate
global.js
file if you prefer modularity.
✅ Result
Here’s what you get:
- A clean, styled form
- Asynchronous submission using just HTML + HMPL
- Validation and feedback—all without custom JS logic
👀 Why Use This Approach?
- No JavaScript Framework Needed: No React, no Angular.
- Declarative Logic: You describe what should happen, not how.
- Simple and Scalable: Great for landing pages, admin tools, and MVPs.
You can even expand this pattern to support multi-step forms, loaders, error handling, or auto-saving with
repeat
intervals.
💬 Final Thoughts
Building interactive web forms no longer requires JavaScript bloat or massive toolchains. With HMPL, you keep your code clean, semantic, and powerful—perfect for developers who love declarative logic and simplicity.
If you liked the article, consider giving HMPL a star! ❤️
Thank you for reading, and happy coding!
Top comments (7)
Thanks!
Sounds interesting.
What is the difference between HTMX and related libs?
Thank you for your feedback!
About differences:
blog.hmpl-lang.dev/blog/2025/05/03...
blog.hmpl-lang.dev/blog/2024/08/10...
Only, there is old syntax in some points, but the essence does not change.
Fantastic App – feature-rich and intuitive.
Thank you! It may be small, but it does the job.
In this example, the registration form on the site was taken, since this is probably the most popular form on the Internet. But, you can think of an application for any other form.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.