Menu docs

Fitur & Build Notes · · 3 menit baca

Membangun form kontak HTMX di Cloudflare Workers — tanpa SPA

Dokumentasi lengkap form kontak tanpa SPA: HTMX partial swap, validasi server dengan Hono JSX, input tidak hilang saat error, dan progressive enhancement — dengan source code dari situs ini.

HTMXHonoCloudflare WorkersTypeScript

Form kontak di situs ini tidak memakai SPA sama sekali: HTMX mengirim POST, server merender ulang partial HTML, dan halaman tidak pernah reload. Validasi terjadi di server (satu-satunya tempat yang bisa dipercaya), dan input pengunjung tidak hilang saat ada error. Ini dokumentasi lengkap cara membangunnya di Cloudflare Workers dengan Hono JSX.

Arsitekturnya

Browser ──POST (hx-post)──▶ Worker (Hono)
   ▲                            │ validasi server
   └──── partial HTML ◀─────────┘ re-render form (error) / kartu sukses

Satu endpoint, tiga kemungkinan respons: form dengan pesan error (422), kartu sukses (200), atau error pengiriman (500). HTMX menukar isi #contact-form-wrap dengan respons apa pun yang datang.

1. Form dengan atribut HTMX

State form dipegang satu interface — nilai input plus map error — sehingga form bisa dirender ulang dengan isian pengunjung tetap utuh:

export interface ContactForm {
  name: string;
  email: string;
  message: string;
  // ...field lain
  errors: Record<string, string>;
}

export const ContactFormView = ({ f, locale }: { f: ContactForm; locale: Locale }) => (
  <form hx-post={href(locale, "/contact")} hx-target="#contact-form-wrap"
        hx-swap="outerHTML" class="space-y-6" novalidate>
    {/* setiap Field menampilkan f.errors[field] di bawah inputnya */}
  </form>
);

novalidate sengaja: validasi browser dimatikan supaya semua pesan error datang dari server — konsisten di semua browser dan tetap jalan tanpa JavaScript.

2. Handler POST di Worker

contactRoutes.post(path, async (c) => {
  const form = await c.req.parseBody();
  const f: ContactForm = {
    name: val("name").trim(),
    email: val("email").trim(),
    message: val("message").trim(),
    errors: {},
  };
  f.errors = validateContact(f, locale);

  if (Object.keys(f.errors).length > 0) {
    return c.html(<ContactFormView f={f} locale={locale} />, 422); // input utuh + error
  }
  await sendContact(c.env, f);            // Resend HTTP API (log-only saat dev)
  return c.html(<ContactSuccess f={f} locale={locale} />);
});

Perhatikan: yang dikembalikan bukan JSON, tapi HTML jadi. Tidak ada state management di browser, tidak ada useState, tidak ada duplikasi logika validasi.

3. Validasi server

function validateContact(f: ContactForm, locale: Locale): Record<string, string> {
  const msgs = resolveContact(locale).errors; // pesan error ikut bahasa halaman
  const errs: Record<string, string> = {};
  if (f.name === "") errs.name = msgs.name;
  if (!EMAIL_RE.test(f.email)) errs.email = msgs.emailInvalid;
  if (f.message.length < 10) errs.message = msgs.message;
  return errs;
}

Kenapa pola ini kupakai

  • Ringan: HTMX 14KB vs framework SPA ratusan KB — di 4G bedanya terasa.
  • Satu sumber kebenaran: validasi cuma ditulis sekali, di server.
  • Progressive enhancement: tanpa JavaScript, form tetap ter-submit sebagai POST biasa.
  • Cocok untuk Workers: respons HTML kecil, cold start nol, tanpa build step client.

Mau fitur seperti ini dibangun untuk bisnismu?