Skip to main content

CSE 331: No-JS guestbook

In this guide we will build a simple no-JS web app that implements a guestbook. Visitors type their name and a message, click a button, and see the running list of everything that has been posted.

Get the final code here:

The browser

For our purposes, the web browser lets the user do exactly two things. It can send a GET request when the user types in a URL or clicks on a link, and it can send a POST request when the user presses the submit button in an HTML form. Other than that, the browser just renders HTML.

A <form> is how the user sends data. Here is a minimal form:

<form action="/submit" method="POST">
  <label>Name: <input type="text" name="name"></label>
  <label>Message: <input type="text" name="message"></label>
  <button type="submit">Post</button>
</form>

The action attribute is the URL the form submits to. The method is the HTTP method to use (GET or POST). The name attribute on each <input> becomes the key in the submitted data; the value is whatever the user typed. When the user clicks the button, the browser sends a HTTP request to the given action URL with all the contained key-value pairs from the input elements.

The browser collects the form fields into a body like name=Alice&message=hello+world, sends a POST request to /submit with that body.

The form with values typed in but not yet submitted. Clicking Post will POST these fields to /submit.

A server: Javalin

A web server is just a program that listens for HTTP requests. Servers can be implemented in any language, but we'll use Java in 331. We've chosen a specific library, Javalin, because it is small and easy to use.

The minimal setup:

Javalin.create(config -> {
    config.staticFiles.add("/public");
    config.routes.get("/", ctx -> ctx.html("<h1>hello</h1>"));
}).start(8331);

This sets up the server to serve files out of the /public subdirectory and to respond to GET requests on / by sending back that little HTML snippet.

To handle the form's POST, register a handler. The convention this guide follows is to put each handler in a named static method on the Server class and pass it as a method reference:

config.routes.post("/submit", Server::handleSubmit);

where handleSubmit is defined as a peer of main:

private static void handleSubmit(Context ctx) {
    String name = ctx.formParam("name");
    String message = ctx.formParam("message");
    System.out.println("Got: " + name + " / " + message);
    ctx.html("<html><body>Thanks " + name + "!</body></html>");
}

The path passed to config.routes.post must match the action attribute on the form. ctx.formParam("name") extracts the value the user typed into the input whose name attribute was "name". ctx.html(...) sends an HTML string back as the response body.

The no-JS guestbook on first load. No messages yet; just the form.

The handler above already builds its response with string concatenation: "<html><body>Thanks " + name + "!</body></html>". One could imagine a more complicated page as well:

private static void handleSubmit(Context ctx) {
    String name = ctx.formParam("name");
    String message = ctx.formParam("message");
    ctx.html(
        "<!DOCTYPE html>" +
        "<html><body>" +
        "<h1>Got it</h1>" +
        "<p>Thanks, " + name + "!</p>" +
        "<p>You said: " + message + "</p>" +
        "<p><a href=\"/\">back</a></p>" +
        "</body></html>"
    );
}

This works, but it's annoying to use string concatenation in this way. We have to escape special characters manually, and it feels strange to mix HTML and Java in the same file.

Instead, we can use a templating engine such as JTE. A "template" is an HTML file with holes in it, that can be filled in with a small programming language. JTE templates live in templates/ with the extension .jte. JTE works well with Javalin:

TemplateEngine engine = TemplateEngine.create(
        new DirectoryCodeResolver(Path.of("templates")),
        ContentType.Html);

Javalin.create(config -> {
    config.fileRenderer(new JavalinJte(engine));
    // ...
});

This configures Javalin to look for templates in the right folder.

A JTE template looks like this:

@import guestbook.Message
@import java.util.List
@param List<Message> messages

<!DOCTYPE html>
<html>
<body>
  <h1>Guestbook</h1>
  @if(messages.isEmpty())
    <p><em>No messages yet.</em></p>
  @else
    <ul>
      @for(var m : messages)
        <li><strong>${m.name()}</strong>: ${m.message()}</li>
      @endfor
    </ul>
  @endif
  <form action="/submit" method="POST">
    <label>Name: <input type="text" name="name"></label><br>
    <label>Message: <input type="text" name="message"></label><br>
    <button type="submit">Post</button>
  </form>
</body>
</html>

The @param line declares that the template expects the server to pass it a List<Message> called messages. You can see that JTE supports @if, @else, @for, and @endfor for control flow. ${expr} interpolates a Java expression into the output.

On the Java side, register a handler and define it to call ctx.render:

config.routes.get("/", Server::handleIndex);
private static void handleIndex(Context ctx) {
    ctx.render("index.jte", Map.of("messages", MESSAGES));
}

The Map argument to render passes the values to the parameters. render constructs the HTML and returns it to the browser.

After a single submission, the JTE-rendered list shows the new entry.

State on the server

The list of messages has to live somewhere. The simplest thing is a static field on the server class:

private static final List<Message> MESSAGES = new ArrayList<>();

Message itself is a Java record:

public record Message(String name, String message) {}

The full POST handler appends to the list:

private static void handleSubmit(Context ctx) {
    String name = ctx.formParam("name");
    String message = ctx.formParam("message");
    if (name != null && !name.isBlank() && message != null && !message.isBlank()) {
        MESSAGES.add(new Message(name, message));
    }
    ctx.render("index.jte", Map.of("messages", MESSAGES));
}
Steady state after several submissions. Each row was rendered server-side by the JTE template's @for loop.