How to process <form> using JS with Hugo?

I’m using FormEasy as a no-cost form backend. It requires form data to be sent as JSON so I need to process the form data before sending it.

My contact form page located at themes\mytheme\layouts\contact\list.html:

{{ define "main" }}

{{ partial "page-header.html" . }}
<section>
  <form id="contactForm">
    <input type="text" id="name" name="name" placeholder="*{{ i18n `name` }}" required>
    <input type="email" id="email" name="email" placeholder="*{{ i18n `email` }}" required>
    <input type="text" id="company" name="company" placeholder="{{ i18n `company` }}">
    <textarea name="message" id="message" placeholder="*{{ i18n `message` }}" required></textarea>
    <input type="hidden" name="source" value="Contact form">
    <button type="submit" value="send">{{ i18n `send` }}</button>
  </form>

  {{ with resources.Get "js/contact.js" }}
    {{ $contactFormParams := dict "formId" "contactForm" "formUrl" site.Params.contact_form_action }}
    {{ if hugo.IsDevelopment }}
      {{ $opts := dict "params" $contactFormParams }}
      {{ with . | js.Build $opts }}
        <script src="{{ .RelPermalink }}"></script>
      {{ end }}
    {{ else }}
      {{ $opts := dict "minify" true "params" $contactFormParams }}
      {{ with . | js.Build $opts | fingerprint }}
        <script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"></script>
      {{ end }}
    {{ end }}
  {{ end }}
</section>

{{ end }}

My form processing JS script located at themes\rarescrap\assets\js\contact.js:

import params from "@params";

let form = document.getElementById(params.formId);
form.onsubmit = function() {
    let formData = new FormData(form);

    fetch(params.formUrl, {
        method: 'POST',
        headers: {
            'Content-Type': 'text/plain;charset=utf-8',
        },
        body: JSON.stringify(Object.fromEntries(formData)),
    })
        .then((res) => res.json())
        .then((data) => console.log('data', data))
        .catch((err) => console.error(err));

    return false;
}

But it not seems how this should be done. Hugo construct contact.js once only so if I use this code somewhere else on my site - I would have parameters of the first generated form.

Could you please explain me how to process my forms with universal JS script?

hugo env
hugo v0.123.0-3c8a4713908e48e6523f058ca126710397aa4ed5+extended windows/amd64 BuildDate=2024-02-19T16:32:38Z VendorInfo=gohugoio
GOOS="windows"
GOARCH="amd64"
GOVERSION="go1.22.0"
github.com/sass/libsass="3.6.5"
github.com/webmproject/libwebp="v1.3.2"

If you want to reuse your form then you need to learn about caching. You can cache a partial:

{{ partialCached "path-to-partial.html" context cacheid }}

Where context might be your form data for this specific page and cacheid might be a unique identifier (maybe printf "%s%s" .Title .Permalink?) and then the partial is created once per this identifier.

Then move your javascript inside of the script tags instead of loading an external script (.Content is the content of the javascript when you print your script tags).

Move the whole form out of that list template and into a partial and then it get’s re-usable.

But from what I see you need your form only once, so move your JS right into the template where you process the form?

Thank you for reply

If you want to reuse your form then you need to learn about caching

Not exactly the form. I mean, I want to reuse the same script across multiple forms with different fields. These forms should different payload to the same endpoint.

Then move your javascript inside of the script tags instead of loading an external script (.Content is the content of the javascript when you print your script tags).

Are you sure this is the right way? In this way the script not seems to be minified.

Also loading script using .Content makes it impossible to debug such script using browser debugger.

I make it work but it still looks too cumbersome.

Firstly I make the form processing function accessible globally:

window.processForm = function(formId, formUrl) {
    let form = document.getElementById(formId);
    form.onsubmit = function() {
        let formData = new FormData(form);
    
        fetch(formUrl, {
            method: 'POST',
            headers: {
                'Content-Type': 'text/plain;charset=utf-8',
            },
            body: JSON.stringify(Object.fromEntries(formData)),
        })
            .then((res) => res.json())
            .then((data) => console.log('data', data))
            .catch((err) => console.error(err));
    
        return false;
    }
}

Then I setup two script blocks: one for load the file, second for call the function:

{{ define "main" }}

{{ partial "page-header.html" . }}
<section class="section bg-white">
  <div class="container">
    <form id="contactForm">
      <input type="text" class="form-control mb-3" id="name" name="name" placeholder="*{{ i18n `name` }}" required>
      <input type="email" class="form-control mb-3" id="email" name="email" placeholder="*{{ i18n `email` }}" required>
      <input type="text" class="form-control mb-3" id="company" name="company" placeholder="{{ i18n `company` }}">
      <textarea name="message" id="message" class="form-control mb-3" placeholder="*{{ i18n `message` }}" required></textarea>
      <input type="hidden" name="source" value="Contact form">
      <button type="submit" value="send" class="btn btn-primary">{{ i18n `send` }}</button>
    </form>

    {{ with resources.Get "js/contact.js" }}
      {{ if hugo.IsDevelopment }}
        {{ with . | js.Build }}
          <script src="{{ .RelPermalink }}"></script>
        {{ end }}
      {{ else }}
        {{ $opts := dict "minify" true }}
        {{ with . | js.Build $opts | fingerprint }}
          <script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"></script>
        {{ end }}
      {{ end }}
    {{ end }}

    <script>
      window.processForm("contactForm", {{ site.Params.contact_form_action }})
    </script>
  </div>
</section>

{{ end }}

But I still have the feeling that this should be done in some better way. Any suggestions?

I’d use a DOMContentLoaded handier. That can simply check which DOM objects exist (eg forms) and act accordingly. There’s no harm in defining your form handler in that function, too