> Some JavaScript on this site that’ll ask the backend to sign up a new user.

This makes me sad. The browser has perfectly good form submission stuff that doesn’t need JavaScript, and there’s seldom any actual reason to require JavaScript. Yet I find it increasingly common that web developers, despite perhaps (hopefully!) still using <form> and <input type=submit>, are somehow unaware of this and assume JavaScript must be used, and use JSON encoding, and trip up over CORs, and… and… Whereas in fact, you only need JavaScript to support seamless content changes based on the form submission (providing feedback or whatever without loading a new version of the page with just that alteration), and it’s normally perfectly reasonable to support normal form submission with just a slight enhancement if JavaScript runs.

Here’s an alteration of the newsletter form, which assumes that /subscribe is a regular HTTP form submission endpoint, accepting application/x-www-form-urlencoded form data and returning a basic message as an HTML fragment (or even plain text):

<div class="hover-div blog-post-container pastel-depth-4 lift-on-hover" id="newsletter_signup" style="border-radius: 14px;">
<h3 style="margin-top: 6px">Newsletter</h3>
<form id="sign_up_for_newsletter_form" method="post" action="https://axleos-blog-newsletter.ew.r.appspot.com/subscribe" target="_blank">
[… seven unchanged lines omitted for brevity …]
</form>
<script>
sign_up_for_newsletter_form.addEventListener("submit", async function(event) {
event.preventDefault();
let response = await fetch(this.action, {
method: "POST",
body: new URLSearchParams(new FormData(this)),
});
newsletter_signup.innerHTML =
response.ok
? "<p>Signup successful!</p>"
: `<p>Signup failed: ${response.statusText} / ${await response.text()}<p>`;
});
</script>

A few notes:

• I’ve added the method, action and target attributes to the <form> element. The consequence of this is that regular form submission, if the JavaScript isn’t run for whatever reason, will submit the data to that URL in a new window, the idea being that the user just gets a new page containing “signup successful” and nothing else. (By contrast, on the current page, submitting the form will submit to the current URL with ?email=… added, appearing to the user as though the page has just reloaded, and it won’t be clear whether or not it worked.)

• I’ve shifted the <script> element from before the form to after it, so that the form exists when the script is run, so that it can do its thing immediately rather than being deferred until the page finishes loading by wrapping it in $(document).ready(function(){…}) (or addEventListener("DOMContentLoaded", function(){…}) would be non-jQuery equivalent, so long as it’s still an inline script). This is very desirable if the scripts are inline anyway, and quantities like this should basically always be inline for the best performance (seriously, inlining all your JS and CSS yields the best performance until surprisingly large sizes—far past 100 KB in practically all deployment and access scenarios, even with a warm cache). You only want ready dances if you’re deliberately loading the script asynchronously (which I will also defend as not unreasonable for something like this: the document is fully interactive without it, it can run later or even not at all and cause no problem).

• I’ve removed jQuery usage because it’s simply not necessary ($("#…") → document.getElementById("…"), .on() → .addEventListener(), $.ajax() → fetch()); all the native alternatives have excellent browser support. I’ve also used native async/await because it makes things a tad nicer. (It’s not a compatibility hazard, because ancient browsers will just call it a syntax error and not execute the script, and thus get the no-JS regular form submission behaviour.)

• I dislike this use of innerHTML with the response text; a little too fragile if you try sending plain text but it ends up including HTML syntax, or if you can get things like unexpected server error HTML from Google App Engine or gunicorn or whatever and now you’re dropping a whole <html>…</html> in there with unknown contents. But I’m matching the original.

• I’ve written just sign_up_for_newsletter_form and newsletter_signup with no document.getElementById("…") wrapping just for the fun of it, showing off my favourite HTML+JavaScript golfing technique. It’s a matter of very ancient history, but part of the spec <https://html.spec.whatwg.org/multipage/nav-history-apis.html...> and it works everywhere. Decide for yourself whether it’s wise. (Personally I think the concerns about it in cases like these are somewhat overblown, but at the very least it’s certainly a technique that should not be scaled.)

• The `new URLSearchParams(…)` wrapping is to get application/x-www-form-urlencoded, for consistency with normal form submission. Just `body: new FormData(this)` uses multipart/form-data encoding, which regular form submission will only use if you’re uploading files via <input type="file">. A little janky, in my opinion, being different from normal form submission, even if simply splitting on type makes a deal of sense. Anyway, all serious HTTP server libraries will support both encodings of form data, so you aren’t likely to need it, but I figured I’d make it consistent anyway, because otherwise experience says mistakes get made. The goal is to minimise the differences between the no-JS and JS pathways, to minimise the chances of the no-JS way accidentally breaking.

• I replaced the button click handler with a form submit handler. Personal preference, to a considerable extent, with no actual practical difference since the button is an <input type="submit">. (Pressing Enter from the email field will trigger clicking on the first submit button: <https://html.spec.whatwg.org/multipage/form-control-infrastr...>.)

• I hold out hope that eventually we might get some kind of auto-resizing iframe (one starting point on the topic: <https://github.com/whatwg/html/issues/555>), so that you could put a response iframe below the form and make it the target. This exposes another approach: put the form inside an iframe, and then just use normal submission entirely. The only problem (and it’s not a tiny problem, unfortunately) is this matter of sizing the iframe.

—⁂—

I would also suggest adding a simple honeypot field to the form: https://news.ycombinator.com/item?id=37058847