DEV Community

Cover image for Create Smart Forms Without JavaScript: The Magic of HTML and AJAX 🦉
Anthony Max Subscriber for HMPL.js

Posted on

42 30 30 31 29

Create Smart Forms Without JavaScript: The Magic of HTML and AJAX 🦉

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!

Lets 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
Enter fullscreen mode Exit fullscreen mode
  • 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;
}
Enter fullscreen mode Exit fullscreen mode

📡 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}`);
});
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

🧠 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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

form


👀 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! ❤️

💎 Star HMPL on GitHub


Thank you for reading, and happy coding!

Thanks

Top comments (7)

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
anthonymax profile image
Anthony Max

Thanks!

Collapse
 
arturampilogov profile image
Artur Ampilogov

Sounds interesting.

What is the difference between HTMX and related libs?

Collapse
 
anthonymax profile image
Anthony Max

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.

Collapse
 
aiomics profile image
aiomics

Fantastic App – feature-rich and intuitive.

Collapse
 
anthonymax profile image
Anthony Max

Thank you! It may be small, but it does the job.

Collapse
 
anthonymax profile image
Anthony Max

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.