Formulir feedback untuk website atau aplikasi

Formulir feedback untuk website atau aplikasi6 menit baca

Buat widget feedback mengambang untuk aplikasi React Anda

Tombol feedback mengambang untuk aplikasi React yang membuka panel kecil, mengirim layar, versi aplikasi, dan akun bersama setiap jawaban, dan tidak pernah kehilangan draf.

Preet SutharPendiri, Ikiform

Render tombol fixed yang membuka panel berbasis Popover API browser, sehingga panel berada di atas setiap lapisan aplikasi Anda tanpa perang z-index dan tertutup saat Escape ditekan. Saat submit, tambahkan route saat ini, versi aplikasi, ID akun, dan viewport ke data formulir, lalu kirim dari browser dengan Ikiform SDK. Kirim email pengguna hanya jika mereka mencentang kotak yang meminta balasan.

Apa yang berbeda dari widget dalam aplikasi

Formulir di halaman menunggu orang menggulir ke sana. Widget ada di setiap layar, yang mengubah apa saja yang bisa salah. Widget harus berada di atas modal, menu, dan toast Anda sendiri. Widget tidak boleh kehilangan pesan yang setengah ditulis saat seseorang mengklik di luarnya. Dan karena pengguna sudah login, Anda sudah tahu siapa mereka dan di mana mereka berada, jadi widget sebaiknya mengirim informasi itu alih-alih menanyakannya.

Perjalanan satu sesi widget feedback, dari klik hingga jawaban tersimpanTombolfeedbackfixed,kananbawahpanelterbukatop layer,attemptkey barupenggunamenilaidanmenulisstate adadi panelsubmitdengankonteksroute,versi,akun,viewportdisimpansekalikey samasaat klikgandapaneltersembunyidrafdisimpanuntukdibukalagiklikSDKklik di luar atau EscapePerjalanan satu sesi widget feedback, dari klik hingga jawaban tersimpanTombol feedbackfixed, kanan bawahpanel terbukatop layer, attempt keybarupengguna menilaidan menulisstate ada di panelsubmit dengankonteksroute, versi, akun,viewportdisimpan sekalikey sama saat klikgandapaneltersembunyidrafdisimpanuntuk dibukalagiklikSDKklik di luar atau Escape
  1. Tombol selalu ada di layar, menempel di pojok kanan bawah.
  2. Mengkliknya membuka panel di top layer browser dan memulai attempt key baru.
  3. Pengguna memilih wajah dan menulis catatan. State formulir tetap ter-mount selama panel terbuka maupun tersembunyi.
  4. Saat submit, widget menambahkan route, versi aplikasi, ID akun, dan viewport, lalu mengirim dengan SDK.
  5. Ikiform menyimpannya sekali, bahkan jika tombol ditekan dua kali, karena kedua kiriman membawa idempotency key yang sama.
  6. Jika pengguna mengklik di luar atau menekan Escape lebih dulu, panel tersembunyi dan draf masih ada di kesempatan berikutnya.
Light dismiss adalah perilaku default Popover API. Karena panel hanya disembunyikan, field teks tetap menyimpan apa yang sudah diketik.

Field untuk aplikasi

Pratinjau

Menjalankan validator Ikiform di browser Anda. Tidak ada yang dikirim.

Bagaimana pengalaman Anda?
Pengguna hanya melihat tiga field pertama. Sisanya ditambahkan lewat kode saat submit. may_reply adalah checkbox, disimpan sebagai true atau false.
FieldBerasal dariAlasan
ratingPengguna, lima wajahField rating dengan max: 5. Wajah lebih cepat dipilih daripada angka
messagePenggunaOpsional. Paling berguna saat wajahnya buruk
may_reply, emailCheckbox, lalu sesi AndaEmail hanya dikirim jika mereka meminta balasan, dan tombol Balas di notifikasi menuju ke mereka
routewindow.location.pathnameLayar yang mana. Dibaca saat submit, setelah navigasi sisi klien apa pun
app_versionBuild AndaMemberi tahu apakah keluhan menyangkut kode yang sudah Anda ubah
account_idSesi AndaMemungkinkan Anda melihat paket dan data mereka. Bukan bukti identitas, lihat di bawah
viewportwindow.innerWidth dan innerHeightKeluhan tata letak sering bergantung pada ukuran

Komponennya

Komponen ini butuh @ikiform/sdk dan React 19, yang punya tipe untuk popover, popoverTarget, dan event toggle. popoverTarget pada tombol mengikatnya ke panel, sehingga browser menangani pembukaan, Escape, dan pengembalian fokus ke tombol.

components/feedback-widget.tsx
import { submit } from "@ikiform/sdk";
import { type FormEvent, type ToggleEvent, useId, useRef, useState } from "react";
import "./feedback-widget.css";

const FORM_ID = "frm_xxxxxxxxxxxxxxxx";
const FACES = ["Awful", "Bad", "Okay", "Good", "Great"];

interface Props {
  accountId: string;
  email: string;
  appVersion: string;
}

export function FeedbackWidget({ accountId, email, appVersion }: Props) {
  const panelId = useId();
  const attempt = useRef("");
  const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
  const [error, setError] = useState("");

  function onToggle(event: ToggleEvent<HTMLDivElement>) {
    if (event.newState !== "open") return;
    attempt.current = crypto.randomUUID();
    if (status === "sent") setStatus("idle");
  }

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    if (data.get("may_reply") === "true") data.set("email", email);
    data.set("route", window.location.pathname);
    data.set("app_version", appVersion);
    data.set("account_id", accountId);
    data.set("viewport", `${window.innerWidth}x${window.innerHeight}`);
    setStatus("sending");
    setError("");
    const result = await submit(FORM_ID, data, { idempotencyKey: attempt.current });
    if (result.ok) {
      setStatus("sent");
      return;
    }
    setError(result.error.fields?.rating?.[0] ?? result.error.message);
    setStatus("idle");
  }

  return (
    <>
      <button type="button" className="feedback-trigger" popoverTarget={panelId}>
        Feedback
      </button>
      <div id={panelId} popover="auto" className="feedback-panel" onToggle={onToggle} aria-label="Send feedback">
        {status === "sent" ? (
          <p role="status">Thanks, that helps.</p>
        ) : (
          <form onSubmit={onSubmit}>
            <fieldset>
              <legend>How is it going?</legend>
              {FACES.map((face, index) => (
                <label key={face}>
                  <input type="radio" name="rating" value={index + 1} required /> {face}
                </label>
              ))}
            </fieldset>
            <label htmlFor={`${panelId}-message`}>Tell us more</label>
            <textarea id={`${panelId}-message`} name="message" rows={4} maxLength={4000} />
            <label>
              <input type="checkbox" name="may_reply" value="true" /> The team can reply to me by email
            </label>
            <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
            {error ? <p role="alert">{error}</p> : null}
            <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send"}</button>
          </form>
        )}
      </div>
    </>
  );
}
  • popover="auto" menaruh panel di top layer saat terbuka. Panel tampil di atas dialog dan menu Anda tanpa z-index, dan tertutup saat pengguna mengklik di tempat lain.
  • Child panel tetap ter-mount saat panel tersembunyi, sehingga draf selamat dari klik tak sengaja di luar. Setelah pengiriman berhasil, baris terima kasih menggantikan formulir, yang mengosongkannya, dan pembukaan berikutnya menampilkan formulir kosong.
  • onToggle membuat key baru setiap kali panel dibuka. submit() mengirimnya sebagai header Idempotency-Key, jadi klik ganda hanya menyimpan satu jawaban. Lihat idempotensi.
  • Email hanya diisi jika kotaknya dicentang. Jika tidak dicentang, jawaban membawa ID akun tetapi tanpa alamat.

Gaya default popover menaruhnya di tengah layar. Aturan ini menyematkannya di atas tombol:

components/feedback-widget.css
.feedback-trigger {
  position: fixed;
  right: 16px;
  bottom: 16px;
}

.feedback-panel {
  inset: auto 16px 64px auto;
  margin: 0;
  width: min(360px, calc(100vw - 32px));
  max-height: calc(100dvh - 96px);
  overflow: auto;
}

Mount sekali saja

Render widget di layout yang membungkus layar untuk pengguna yang login, agar muncul di mana-mana dan tetap ada saat navigasi. Di App Router Next.js, layout adalah server component yang membaca sesi dan meneruskan props biasa ke widget klien:

app/(app)/layout.tsx
import type { ReactNode } from "react";
import { FeedbackWidget } from "@/components/feedback-widget";
import { getSession } from "@/lib/session";

export default async function AppLayout({ children }: { children: ReactNode }) {
  const session = await getSession();
  return (
    <>
      {children}
      {session ? (
        <FeedbackWidget
          accountId={session.accountId}
          email={session.email}
          appVersion={process.env.NEXT_PUBLIC_APP_VERSION ?? "dev"}
        />
      ) : null}
    </>
  );
}

Tandai file widget dengan "use client" di App Router. Di aplikasi Vite atau React Router, render di samping router outlet Anda. Cara kerjanya sama.

Apa yang bisa dan tidak bisa dibuktikan browser

Widget ini mengirim langsung dari browser dengan ID publik formulir. Itu kompromi yang tepat untuk feedback: tidak ada server route yang perlu dirawat, dan meta.user_agent milik Ikiform adalah browser asli pengguna. Itu juga berarti siapa pun bisa mengirim account_id apa pun. Gunakan untuk mencari informasi, jangan pernah untuk memberikan akses apa pun.

Jika akun harus bisa dipercaya, misalnya untuk menampilkan feedback di dalam alat admin, kirim dari server Anda dengan secret key dan tambahkan akun dari sesi di sana. Versi server di pilar menunjukkan polanya; pilih rate limit Relaxed atau Custom, karena setiap kiriman kemudian berasal dari IP server Anda.

Pastikan berfungsi

  1. Di Allowed domains formulir, tambahkan domain situs Anda, ditambah localhost selama pengembangan. Biarkan kosong untuk menerima kiriman dari mana saja.
  2. Buka panel, ketik catatan, klik di luar, lalu buka lagi. Catatannya masih ada.
  3. Buka modal di aplikasi Anda, lalu widget. Panel tampil di atas modal.
  4. Tekan Escape. Panel tertutup dan fokus kembali ke tombol Feedback.
  5. Pilih wajah dan klik dua kali Kirim. Submissions menampilkan satu jawaban, dengan route, app_version, dan account_id terisi.
  6. Kirim sekali dengan kotak balasan dicentang. Tombol Balas di email notifikasi menuju ke pengguna.

Hal yang sering menjebak

  • Jauhkan tombol dari elemen mengambang Anda sendiri, seperti gelembung chat atau banner cookie. Pindahkan salah satunya, atau buka widget dari item menu sebagai gantinya.
  • Jangan tampilkan tombol di setiap layar checkout atau editor tempat orang sedang berkonsentrasi. Sembunyikan di sana dengan prop.
  • Popover API didukung di Chrome, Edge, Firefox, dan Safari versi terbaru. Jika Anda harus mendukung browser lama, periksa dulu sebelum mengandalkannya.
  • Setiap field yang ditambahkan lewat kode harus ada di definisi formulir, atau Ikiform menolak kiriman dengan error Unknown field.
  • Jangan kirim seluruh state aplikasi. Route, versi, akun, dan ukuran menjawab sebagian besar pertanyaan; selebihnya adalah soal privasi.

Pemecahan masalah

MasalahYang harus dilakukan
403 origin_not_allowed, atau setiap kiriman masuk ke spamDomain halaman tidak ada di Allowed domains formulir. Tambahkan, ditambah localhost untuk uji lokal. Subdomain dari domain yang terdaftar diizinkan.
403 challenge_requiredSpam check formulir diatur ke Ikiform dan kiriman fetch tidak membawa token. Ambil token dengan challenge(formId) dari @ikiform/sdk sebelum setiap pengiriman, atau matikan pemeriksaannya.
Panel muncul di tengah layarStylesheet tidak termuat, jadi gaya popover default browser yang berlaku. Periksa import CSS, atau atur inset dan margin pada panel.
422 validation_failedKiriman tidak cocok dengan definisi field formulir. error.fields menyebutkan setiap field dan alasannya. Samakan name input dengan ID field.

Pertanyaan

Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.