npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

kikyreact-gridform

v0.2.2

Published

Kiky React Grid & Form: tabel/grid + form dinamis (masking, validation, array items) dengan i18n.

Downloads

11

Readme

🧩 Kiky React Grid & Form

Tabel/Grid + Form Dinamis untuk React 16–19 — mendukung masking, validasi, array items, dan i18n (termasuk locale Sunda su).

{
  "name": "kikyreact-gridform",
  "version": "0.2.1",
  "main": "dist/index.cjs",
  "module": "dist/index.js",
  "types": "dist/index.d.ts",
  "peerDependencies": {
    "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
    "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
  }
}

npm version jsDelivr License: MIT React TypeScript

Pakai varian paket lain (kikygridtablejangsaria)? Ganti impor:

- import { Grid, KikyForm, su } from "kikyreact-gridform";
+ import { Grid, KikyForm, su } from "kikygridtablejangsaria";

📦 Instalasi

npm install kikyreact-gridform
# atau
yarn add kikyreact-gridform
# atau
pnpm add kikyreact-gridform

Tambahkan stylesheet:

import "kikyreact-gridform/styles.css";

🚀 Penggunaan Singkat (Client-side)

import * as React from "react";
import { Grid, su } from "kikyreact-gridform";
import type { Column } from "kikyreact-gridform";
import "kikyreact-gridform/styles.css";

type Row = {
  id: number;
  name: string;
  category: "A" | "B" | "C";
  price: number;
  created_at: string;
};

const localRows: Row[] = Array.from({ length: 123 }).map((_, i) => ({
  id: i + 1,
  name: `Produk ${i + 1}`,
  category: (["A", "B", "C"] as const)[i % 3],
  price: Math.round(10000 + Math.random() * 90000),
  created_at: new Date(Date.now() - i * 86400000).toISOString(),
}));

export default function AppClient() {
  const columns: Column<Row>[] = [
    { id: "id", header: "ID", align: "right", sortable: true },
    { id: "name", header: "Nami", sortable: true, editable: true },
    {
      id: "category",
      header: "Kategori",
      sortable: true,
      editable: true,
      editor: "select",
      options: [
        { label: "A", value: "A" },
        { label: "B", value: "B" },
        { label: "C", value: "C" },
      ],
    },
    { id: "price", header: "Harga", align: "right", sortable: true, editable: true, editor: "number" },
    { id: "created_at", header: "Diciptakeun", sortable: true },
  ];

  const gfilters = [
    {
      id: "category",
      label: "Kategori",
      type: "select" as const,
      options: [
        { label: "— Sadayana —", value: "" },
        { label: "A", value: "A" },
        { label: "B", value: "B" },
        { label: "C", value: "C" },
      ],
    },
    { id: "name", label: "Nami", type: "text" as const, placeholder: "Cari nama produk…" },
  ];

  const BASE_PATH = "/products";
  const navigateTo = (row: Row | null, action: "edit" | "add" | "detail" | "delete") => {
    switch (action) {
      case "edit": return row ? `${BASE_PATH}/${row.id}/edit` : "#";
      case "add": return `${BASE_PATH}/new`;
      case "detail": return row ? `${BASE_PATH}/${row.id}` : "#";
      case "delete": return row ? `${BASE_PATH}/${row.id}/delete` : "#";
      default: return "#";
    }
  };

  return (
    <div style={{ padding: 16 }}>
      <h2 style={{ marginBottom: 12 }}>Client-side data</h2>
      <Grid<Row>
        columns={columns}
        data={localRows}
        pageSize={10}
        locale={{ ...su, clear: "Beresihan" }}
        filterUI="both"
        globalFilters={gfilters as any}
        addMode="navigate"
        editMode="navigate"
        deleteMode="modal"
        detailMode="expand"
        navigateTo={(row, action) => navigateTo(row as Row | null, action)}
        onSave={(row) => console.log("SAVE", row)}
        onCreate={(row) => console.log("CREATE", row)}
        onDelete={(row) => console.log("DELETE", row)}
      />
    </div>
  );
}

🌐 Server-side (Provider)

Gunakan provider yang menerima DataRequest<T> dan mengembalikan DataResponse<T>.

import * as React from "react";
import { Grid, su } from "kikyreact-gridform";
import type { Column, DataRequest, DataResponse } from "kikyreact-gridform";
import "kikyreact-gridform/styles.css";

type Row = {
  id: number;
  name: string;
  category: "A" | "B" | "C";
  price: number;
  created_at: string;
};

// Contoh ke endpoint POST /api/products
const serverProvider = async (req: DataRequest<Row>): Promise<DataResponse<Row>> => {
  const res = await fetch("/api/products", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
};

export default function AppServerTwoGrids() {
  const columns: Column<Row>[] = [
    { id: "id", header: "ID", align: "right", sortable: true, filterable: true },
    { id: "name", header: "Nami", sortable: true, editable: true, filterable: true },
    {
      id: "category",
      header: "Kategori",
      sortable: true,
      editable: true,
      filterable: true,
      editor: "select",
      options: [{ label: "A", value: "A" }, { label: "B", value: "B" }, { label: "C", value: "C" }],
    },
    { id: "price", header: "Harga", align: "right", sortable: true, editable: true, editor: "number", filterable: true },
    { id: "created_at", header: "Diciptakeun", sortable: true, filterable: true },
  ];

  const gfilters = [
    {
      id: "category",
      label: "Kategori",
      type: "select" as const,
      options: [
        { label: "— Sadayana —", value: "" },
        { label: "A", value: "A" },
        { label: "B", value: "B" },
        { label: "C", value: "C" },
      ],
    },
    { id: "name", label: "Nama", type: "text" as const },
  ];

  return (
    <div style={{ padding: 16, display: "grid", gap: 24 }}>
      {/* GRID ATAS: global + per-kolom */}
      <section>
        <h2 style={{ marginBottom: 8 }}>Server-side: Global & Per-Column Filters</h2>
        <Grid<Row>
          columns={columns}
          data={serverProvider}
          pageSize={10}
          locale={{ ...su, clear: "Beresihan" }}
          filterUI="both"
          globalFilters={gfilters as any}
          editMode="modal"
          addMode="modal"
          deleteMode="modal"
          detailMode="expand"
          onSave={(row)=>console.log("SAVE", row)}
          onCreate={(row)=>console.log("CREATE", row)}
          onDelete={(row)=>console.log("DELETE", row)}
        />
      </section>

      {/* GRID BAWAH: hanya filter per-kolom */}
      <section>
        <h2 style={{ marginBottom: 8 }}>Server-side: Search by Column Only</h2>
        <Grid<Row>
          columns={columns}
          data={serverProvider}
          pageSize={10}
          locale={{ ...su, clear: "Beresihan" }}
          filterUI="perColumn"
          showFilters={true}
          editMode="modal"
          addMode="modal"
          deleteMode="modal"
          detailMode="expand"
          onSave={(row)=>console.log("SAVE", row)}
          onCreate={(row)=>console.log("CREATE", row)}
          onDelete={(row)=>console.log("DELETE", row)}
        />
      </section>
    </div>
  );
}

Bentuk request (DataRequest<T>):

type DataRequest<T> = {
  page: number;                  // halaman (1-based)
  pageSize: number;              // baris per halaman
  search?: string;               // pencarian global
  sort?: { id: keyof T; dir: "asc" | "desc" } | null;
  filters?: Record<string, string>; // filter global & per-kolom
};

Bentuk response (DataResponse<T>):

type DataResponse<T> = {
  rows: T[];
  total: number; // total baris untuk pagination server
};

🧰 Form Dinamis (KikyForm)

import * as React from "react";
import { KikyForm } from "kikyreact-gridform";
import type { FormSchema, Field } from "kikyreact-gridform";
import "kikyreact-gridform/styles.css";
import "bootstrap/dist/css/bootstrap.min.css";

export default function ProductsNew() {
  const schema: FormSchema = {
    title: "Tambah Produk",
    submitLabel: "Simpan",
    cancelLabel: "Batal",
    fields: [
      { id: "supplier_name", label: "Nama Pemasok", kind: "text", required: true, minLength: 3, placeholder: "PT Contoh Abadi" } as Field,
      { id: "supplier_email", label: "Email Pemasok", kind: "email", required: true, placeholder: "[email protected]" } as Field,
      { id: "supplier_phone", label: "Telepon Pemasok", kind: "phone", mask: "phone", placeholder: "+62 ..." } as Field,
      { id: "supplier_website", label: "Website", kind: "url", placeholder: "https://contoh.com" } as Field,

      { id: "name", label: "Nama Produk", kind: "text", required: true, placeholder: "Nama produk" } as Field,
      { id: "slug", label: "Slug", kind: "text", mask: "slug", helpText: "Slug otomatis dari nama (bisa disunting)", placeholder: "nama-produk" } as Field,
      {
        id: "category",
        label: "Kategori",
        kind: "select",
        required: true,
        placeholder: "— Pilih kategori —",
        options: [{ label: "A", value: "A" }, { label: "B", value: "B" }, { label: "C", value: "C" }],
      } as Field,
      {
        id: "labels",
        label: "Label",
        kind: "select",
        multiple: true,
        placeholder: "Tahan Ctrl/⌘ untuk multi-pilih",
        options: [{ label: "Promo", value: "promo" }, { label: "Baru", value: "new" }, { label: "Best Seller", value: "best" }],
      } as Field,
      {
        id: "status",
        label: "Status",
        kind: "radio",
        options: [{ label: "Draft", value: "draft" }, { label: "Published", value: "published" }, { label: "Archived", value: "archived" }],
        required: true,
      } as Field,
      { id: "is_active", label: "Aktif", kind: "checkbox", helpText: "Centang untuk mengaktifkan produk" } as Field,

      { id: "base_price", label: "Harga Dasar (IDR)", kind: "currency", mask: "currency", required: true, placeholder: "100.000" } as Field,
      {
        id: "discount_percent",
        label: "Diskon (%)",
        kind: "text",
        mask: "percent",
        helpText: "0–100",
        validate: (v) => {
          const n = Number(String(v ?? "").replace(/[^\d.]/g, ""));
          if (!Number.isFinite(n)) return "Harus angka 0–100";
          if (n < 0 || n > 100) return "Rentang 0–100";
          return undefined;
        },
      } as Field,
      { id: "stock", label: "Stok (slider)", kind: "range", min: 0, max: 1000, step: 5, showValue: true } as Field,

      { id: "available_from", label: "Tersedia Mulai", kind: "date" } as Field,
      { id: "available_time", label: "Jam Tersedia", kind: "time" } as Field,
      { id: "available_at", label: "Tanggal & Waktu Rilis", kind: "datetime" } as Field,
      { id: "theme_color", label: "Warna Tema", kind: "color" } as Field,

      { id: "images", label: "Gambar Produk", kind: "file", accept: "image/*", multiple: true, helpText: "Bisa unggah lebih dari satu gambar" } as Field,

      { id: "sku", label: "SKU", kind: "text", pattern: /^[A-Z0-9-]{6,20}$/, patternMessage: "Gunakan huruf besar/angka/tanda '-' (6–20)" } as Field,
      { id: "tags", label: "Tags", kind: "tags", placeholder: "Ketik lalu Enter (,)" } as Field,
      { id: "note", label: "Catatan", kind: "textarea", placeholder: "Muncul saat status Draft", visible: (vals) => (vals as any).status === "draft" } as Field,

      {
        id: "items",
        label: "Daftar Varian/Item",
        kind: "array",
        min: 1,
        addLabel: "Tambah baris item",
        of: [
          { id: "name", label: "Nama Item", kind: "text", required: true, minLength: 2, placeholder: "Varian ukuran/warna" } as Field,
          { id: "category", label: "Kategori", kind: "select", options: [{ label: "A", value: "A" }, { label: "B", value: "B" }, { label: "C", value: "C" }], placeholder: "Kategori item" } as Field,
          { id: "price", label: "Harga (IDR)", kind: "currency", mask: "currency", required: true, placeholder: "50.000" } as Field,
          { id: "qty", label: "Qty", kind: "number", min: 1, max: 9999, step: 1, required: true, placeholder: "1" } as Field,
          { id: "active", label: "Aktif?", kind: "checkbox" } as Field,
        ],
      } as Field,
    ],
  };

  const onSubmit = async (values: Record<string, unknown>) => {
    const toNumber = (s: unknown) => Number(String(s ?? "0").replace(/[^\d]/g, "") || 0);
    const toPercent = (s: unknown) => {
      const n = Number(String(s ?? "0").replace(/[^\d.]/g, ""));
      return Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : 0;
    };
    const items = (Array.isArray(values.items) ? (values.items as any[]) : []).map((it) => ({
      ...it, price: toNumber(it.price), qty: Number(it.qty ?? 0),
    }));
    const payload = {
      ...values,
      base_price: toNumber(values.base_price),
      discount_percent: toPercent(values.discount_percent),
      items,
      images: Array.isArray(values.images) ? (values.images as File[]).map((f) => f.name)
        : (values.images as File | null)?.name ?? null,
    };
    await fetch("/api/products/bulk", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    alert("Produk tersimpan (mock)!");
  };

  return (
    <div className="container py-4">
      <div className="card shadow-sm border-0">
        <div className="card-header bg-primary text-white fw-semibold">Tambah Produk</div>
        <div className="card-body">
          <KikyForm
            schema={schema}
            onSubmit={onSubmit}
            onCancel={() => history.back()}
            initialValues={{ status: "draft", is_active: true, stock: 100, items: [{ name: "", category: "", price: "", qty: 1, active: true }] }}
          />
        </div>
      </div>
    </div>
  );
}

🗺️ Routing (React Router)

import React from "react";
import ReactDOM from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";

import AppClient from "./AppClient";
import AppServer from "./AppServer";
import ProductsNewPage from "./products/new/page";

import "kikyreact-gridform/styles.css";

const router = createBrowserRouter([
  { path: "/", element: <AppClient /> },
  { path: "/server", element: <AppServer /> },
  { path: "/products/new", element: <ProductsNewPage /> },
]);

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);

🌎 CDN (jsDelivr)

ESM (type="module") — tanpa bundler:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/styles.css" />
<script type="module">
  import { Grid, KikyForm, su } from "https://cdn.jsdelivr.net/npm/[email protected]/dist/index.js";
  // ... gunakan Grid/KikyForm
</script>

UMD (global window.KikyUI) — jika kamu menyediakan dist/index.umd.js:

<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/styles.css" />
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/index.umd.js"></script>
<script>
  const { Grid, KikyForm, su } = window.KikyUI;
</script>

Belum ada file UMD? Pakai opsi ESM, atau tambah build UMD via Rollup/tsup.


🧩 API Ringkas

Grid<T> (props inti)

  • columns: Column<T>[] — definisi kolom (id, header, editor, options, align, sortable, filterable)
  • data: T[] | (req: DataRequest<T>) => Promise<DataResponse<T>> — data lokal atau provider server
  • pageSize?: number — default 10
  • locale?: Partial<Locale> — label (tersedia su)
  • filterUI?: "perColumn" | "global" | "both", showFilters?: boolean, globalFilters?: GlobalFilter<T>[]
  • editMode?: "inline" | "modal" | "navigate", addMode?, deleteMode?, detailMode?: "none" | "expand" | "navigate"
  • navigateTo?: (row, action) => string — untuk mode navigate
  • onSave?, onCreate?, onDelete?

KikyForm

  • schema: FormSchema — field: text, email, number, currency, date/time/datetime, select (single/multiple), radio, checkbox, tags, file, range, array
  • initialValues?: Record<string, unknown>
  • onSubmit(values), onCancel?()

🧪 Kompatibilitas React 16–19

  • API React 18 yang spesifik dibungkus agar aman di 16/17 (mis. useSyncExternalStore via shim).
  • Hindari react-dom/client di library inti; biarkan aplikasi yang melakukan mount.

🐞 Troubleshooting

  • E403 “You cannot publish over the previously published versions” → naikkan versi (npm version patch) lalu npm publish.
  • CDN belum update → purge cache via jsDelivr Purge.
  • Peer dep mismatch → pastikan versi React/ReactDOM sesuai rentang peerDependencies.

🛠️ Skrip Build

npm run build         # build CJS + ESM + d.ts
npm run build:watch   # watch mode
npm run dev           # jalankan contoh (examples/basic)
npm run lint          # linting

🪪 Lisensi

MIT © Zaenal Muttaqien