kikyreact-gridform
v0.2.2
Published
Kiky React Grid & Form: tabel/grid + form dinamis (masking, validation, array items) dengan i18n.
Downloads
11
Maintainers
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"
}
}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-gridformTambahkan 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 serverpageSize?: number— default10locale?: Partial<Locale>— label (tersediasu)filterUI?: "perColumn" | "global" | "both",showFilters?: boolean,globalFilters?: GlobalFilter<T>[]editMode?: "inline" | "modal" | "navigate",addMode?,deleteMode?,detailMode?: "none" | "expand" | "navigate"navigateTo?: (row, action) => string— untuk modenavigateonSave?,onCreate?,onDelete?
KikyForm
schema: FormSchema— field: text, email, number, currency, date/time/datetime, select (single/multiple), radio, checkbox, tags, file, range, arrayinitialValues?: Record<string, unknown>onSubmit(values),onCancel?()
🧪 Kompatibilitas React 16–19
- API React 18 yang spesifik dibungkus agar aman di 16/17 (mis.
useSyncExternalStorevia shim). - Hindari
react-dom/clientdi library inti; biarkan aplikasi yang melakukan mount.
🐞 Troubleshooting
- E403 “You cannot publish over the previously published versions” → naikkan versi (
npm version patch) lalunpm 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
