YaYaw TableYaYaw Table

Configuration pas à pas avec un exemple concret Next.js

Provider & Setup

DataTable est le point d'entrée unique. Vous passez la configuration et les actions en props, puis DataTable compose TableProvider en interne.

Breaking change : YaYaw Table ne crée plus de QueryClient interne. Vous devez fournir un client TanStack Query partagé via QueryClientProvider (recommandé) ou passer explicitement une instance queryClient partagée.

Cette page montre un setup concret à copier, puis à adapter.

Setup pas à pas (Next.js App Router)

1. Ajouter NuqsAdapter dans votre root layout

Si vous voulez conserver le tri, les filtres, la pagination et la visibilité dans les paramètres d'URL, entourez l'app avec NuqsAdapter :

// app/layout.tsx
import { NuqsAdapter } from "nuqs/adapters/next/app";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="fr">
      <body>
        <NuqsAdapter>{children}</NuqsAdapter>
      </body>
    </html>
  );
}

2. Créer vos actions de table (minimum : list)

La table appelle vos actions pour les données et le CRUD. La seule action obligatoire est list.

// app/products/actions/products.ts
"use server";

type ListProductsParams = {
  advancedFilters?: unknown[];
  filters?: Record<string, unknown>;
  limit?: number;
  orderBy?: Record<string, "asc" | "desc">;
  page?: number; // 1-based
  search?: string;
};

const products = [
  { id: "1", name: "MacBook Pro", brand: "Apple", price: 2499, isActive: true },
  { id: "2", name: "ThinkPad X1", brand: "Lenovo", price: 1899, isActive: true },
  { id: "3", name: "XPS 13", brand: "Dell", price: 1599, isActive: false },
];

export async function listProducts(params: ListProductsParams) {
  const { limit = 10, page = 1, search = "" } = params;

  const filtered = products.filter((product) =>
    product.name.toLowerCase().includes(search.toLowerCase())
  );

  const start = (page - 1) * limit;
  const end = start + limit;
  const paged = filtered.slice(start, end);

  return {
    data: paged,
    meta: {
      pageCount: Math.max(1, Math.ceil(filtered.length / limit)),
      totalCount: filtered.length,
    },
  };
}

export async function createProduct(data: Record<string, unknown>) {
  return { success: true, data };
}

export async function updateProduct(id: string, data: Record<string, unknown>) {
  return { success: true, data: { id, ...data } };
}

export async function deleteProduct(id: string) {
  return { success: true, data: { id } };
}

3. Créer getTableConfig et getTableActions

Gardez le setup dans un seul fichier pour maintenir facilement chaque tableType :

// app/products/setup/table-config.ts
import {
  createProduct,
  deleteProduct,
  listProducts,
  updateProduct,
} from "../actions/products";

export const getTableConfig = (tableType: string) => {
  if (tableType !== "products") {
    return;
  }

  return {
    defaultPageSize: 10,
    enableColumnDnd: true,
    enableColumnDragDropByDefault: false,
    enableColumnFilters: true,
    enableGrouping: false,
    enableMultiRowSelection: true,
    enablePagination: true,
    enableRowDragDrop: false,
    enableRowClickEdit: false,
    enableRowSelection: true,
    enableSorting: true,
    pageSizeOptions: [10, 20, 50],
    columns: {
      definitions: [
        { id: "name", type: "text", header: "Name", enableSorting: true, enableColumnFilter: true },
        { id: "brand", type: "text", header: "Brand", enableSorting: true, enableColumnFilter: true },
        { id: "price", type: "number", header: "Price", enableSorting: true, enableColumnFilter: true },
        { id: "isActive", type: "boolean", header: "Active", enableSorting: true, enableColumnFilter: true },
        { id: "actions", type: "actions", header: "Actions", enableSorting: false, enableColumnFilter: false },
      ],
      order: ["select", "name", "brand", "price", "isActive", "actions"],
      visible: ["select", "name", "brand", "price", "isActive", "actions"],
      mandatory: ["name"],
      sort: [{ id: "name", desc: false }],
    },
    translations: {
      namespace: "table.products",
      keys: {
        title: "Products",
        description: "Manage your product catalog",
      },
    },
  };
};

export const getTableActions = (tableType: string) => {
  if (tableType !== "products") {
    return;
  }

  return {
    list: listProducts,
    create: createProduct,
    update: updateProduct,
    delete: deleteProduct,
  };
};

4. Rendre DataTable dans un QueryClientProvider partagé

getTableConfig et getTableActions sont des fonctions, donc le composant qui les passe doit être un Client Component.

// app/products/page.tsx
"use client";

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { DataTable } from "@/components/ui/yayaw-table";
import { getTableActions, getTableConfig } from "./setup/table-config";

const queryClient = new QueryClient();

export default function ProductsPage() {
  return (
    <QueryClientProvider client={queryClient}>
      <DataTable
        tableType="products"
        getTableConfig={getTableConfig}
        getTableActions={getTableActions}
        title="Products"
        description="Server-side pagination, filtering and sorting"
      />
    </QueryClientProvider>
  );
}

5. Optionnel : ajouter getFormConfig pour les dialogs create/edit

Si vous voulez les formulaires create/edit/bulk-edit intégrés, fournissez getFormConfig :

// app/products/setup/form-config.ts
export const getFormConfig = (formType: string) => {
  if (formType !== "products") {
    return;
  }

  return {
    id: "products",
    fields: [
      { type: "text", name: "name", label: "Name", required: true },
      { type: "number", name: "price", label: "Price", required: true },
      { type: "switch", name: "isActive", label: "Active" },
    ],
  };
};

Puis passez-la :

<DataTable
  tableType="products"
  getTableConfig={getTableConfig}
  getTableActions={getTableActions}
  getFormConfig={getFormConfig}
/>

Vérifications de configuration

Après les 4 étapes principales, votre table doit :

  • Charger les données via list(params).
  • Garder tri, filtres et pagination dans les paramètres URL.
  • Appeler create/update/delete via les actions de ligne.
  • Utiliser vos colonnes/ordre/visibilité définis dans getTableConfig.

getTableConfig

getTableConfig(tableType) doit retourner :

  • Les clés de comportement de table (enableRowSelection, enableRowClickEdit, enableSorting, enableColumnFilters, defaultPageSize, pageSizeOptions, export, bulkExport, actionsAsIcons).
  • enableRowClickEdit ouvre le drawer d'édition au clic ligne et ne peut pas être combiné avec urlDisplayMode: "row-link" ni avec l'inline edit.
  • columnsdefinitions (array de { id, type, header, enableSorting?, enableColumnFilter? }), order, visible, mandatory, sort.
  • translations (optionnel) – namespace, keys pour title/description.

Voir Configuration et Columns pour toutes les options.

Dans YaYaw Table, filtrage, pagination et tri sont server-side.

getTableActions

Retourne un objet avec les méthodes d'action. La table les appelle quand l'utilisateur trie, filtre, pagine ou exécute des actions CRUD/bulk.

  • listObligatoire pour les données server-driven. Signature : (params) => Promise<{ data, meta: { pageCount, totalCount } }>.
  • create, update, delete, duplicate – Optionnelles ; utilisées pour les actions de ligne et le comportement de masse par défaut.
  • bulkDelete, bulkCopy, bulkUpdate – Optionnelles ; utilisées quand l'utilisateur lance des actions de masse.

Voir Actions et Server-side & Server Actions.

getFormConfig

Retourne la configuration de formulaire par type (ex. "products", "products-bulk"). Utilisée pour construire les dialogs create/edit et bulk edit. Structure : tableau de champs avec name, type, label, required, etc. Voir Formulaires et champs collection pour le modèle complet des champs, y compris les éditeurs natifs de tableaux.

Champs booléens : dans les formulaires catalogue, les booléens sont affichés en Switch par défaut (type: "switch" ou type: "checkbox"). Si vous voulez explicitement une checkbox, définissez variant: "checkbox".

{
  type: "switch",
  name: "isActive",
  label: "Active",
}

Champs collection

Utilisez type: "collection" quand une valeur de formulaire est un tableau d'objets et que les utilisateurs doivent ajouter, éditer, supprimer, réordonner et valider des items sans écrire un éditeur custom dans l'application consommatrice. La collection reste contrôlée par la valeur du formulaire : YaYaw Table écrit le prochain tableau via l'API de champ, sans état JSON opaque à synchroniser.

Le champ collection fournit nativement :

  • Une vue structurée des items avec colonnes configurables.
  • Un état vide clair.
  • Un bouton d'ajout simple ou plusieurs actions d'ajout via createActions.
  • Des actions éditer, supprimer, monter et descendre.
  • Une dialog interne pour créer ou éditer un item.
  • Des erreurs par ligne via validateItem.
  • Des erreurs globales via validateItems.
  • Le blocage du submit quand la collection est invalide.
import { defineFormConfig } from "@/components/ui/yayaw-table/components/forms";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { z } from "zod";

const FeatureSchema = z.object({
  features: z.array(
    z.object({
      label: z.string().min(1),
      enabled: z.boolean(),
    })
  ),
});

export const featureForm = defineFormConfig({
  id: "features",
  schema: FeatureSchema,
  defaultValues: {
    features: [],
  },
  fields: [
    {
      type: "collection",
      name: "features",
      label: "Features",
      description: "Manage the product feature list.",
      addLabel: "Add feature",
      itemLabel: "feature",
      emptyLabel: "No features yet.",
      columns: [
        { id: "label", header: "Label" },
        {
          id: "enabled",
          header: "Enabled",
          render: (item) => (item.enabled ? "Yes" : "No"),
        },
      ],
      createItem: () => ({ label: "", enabled: true }),
      renderItemForm: ({ item, onChange, disabled }) => (
        <div className="space-y-3">
          <Input
            disabled={disabled}
            onChange={(event) =>
              onChange({ ...item, label: event.target.value })
            }
            value={String(item.label ?? "")}
          />
          <Switch
            checked={item.enabled === true}
            disabled={disabled}
            onCheckedChange={(enabled) => onChange({ ...item, enabled })}
          />
        </div>
      ),
      validateItem: (item) =>
        typeof item.label === "string" && item.label.length > 0
          ? []
          : ["Label is required"],
      validateItems: (items) =>
        items.length > 0 ? [] : ["Add at least one feature"],
    },
  ],
});

validateItem marque les lignes invalides, validateItems affiche des erreurs globales, et les deux validateurs sont branchés au champ TanStack Form pour empêcher le submit. La validation Zod du formulaire reste active sur la même valeur.

Pour un JSON de menu, utilisez createActions afin de créer plusieurs formes d'items (link, group, themeToggle, languageToggle) et utilisez CollectionEditor dans le formulaire d'un groupe pour éditer group.items. La page Formulaires et champs collection détaille ce cas avec les collections imbriquées et le choix entre collection et custom.

Traductions de formulaire : le formulaire catalogue utilise le même objet DataTableTranslations que la table. Vous devez ajouter les clés form et value (ex. form.name, form.submit, value.string_placeholder) et utiliser labelKey / optionKeys dans vos champs pour résoudre labels et placeholders. Voir Translations Reference — Form translations pour l'exemple complet.

Props complètes (point d'entrée unique)

Toutes les props sont optionnelles sauf tableType.

PropTypeDescription
tableTypestringObligatoire. Identifiant de la table (ex. "products").
getTableConfig(tableType: string) => Config | undefinedRetourne colonnes, options, tri par défaut, visibilité, clés de traduction.
getTableActions(tableType: string) => TableActions | undefinedRetourne list, create, update, delete, duplicate, bulkDelete, bulkCopy, bulkUpdate.
getFormConfig(formType: string) => FormConfig | undefinedRetourne les champs des formulaires create/edit/bulk.
translationsDataTableTranslationsSurcharge les chaînes UI par défaut.
localestringLocale pour les traductions (par défaut "en").
queryClientQueryClientClient explicite optionnel ; doit correspondre à l'instance du QueryClientProvider.
titlestringTitre de la table au-dessus de la barre d'outils.
descriptionstringDescription courte sous le titre.
enableToolbarbooleanAffiche la barre d'outils (filtres, tri, colonnes, etc.).
enableAdvancedFiltersbooleanActive le panneau de filtres avancés.
columnTypeMappingRecord<string, 'text' | 'number' | 'date' | 'option' | 'multiOption'>Mappe vos types backend vers les types internes de colonnes/filtres.
onExport(rows) => void | Promise<void>Surcharge l'export de la barre d'outils (toutes les lignes filtrées).
onBulkExport(rows) => void | Promise<void>Surcharge l'export bulk (lignes sélectionnées).
onBulkEdit, onBulkDelete, onBulkCopy(rows) => BulkActionResult | voidSurcharge des actions de masse ; contrat explicite recommandé pour un comportement déterministe.
loadingOverlayReactNodeUI de chargement personnalisée.
TitleComponent, DescriptionComponentComponentTypeComposants personnalisés pour titre/description.

Voir URL state (Nuqs) pour la synchronisation URL.

Erreurs de setup fréquentes

  • Retourner un page en 0-based dans list ; YaYaw Table attend un index 1-based.
  • Oublier meta.pageCount/meta.totalCount dans la réponse de list.
  • Utiliser un tableType différent entre DataTable, getTableConfig et getTableActions.
  • Rendre DataTable dans un Server Component tout en passant des fonctions.
  • Oublier le QueryClientProvider partagé (erreur runtime QueryClient manquant).

Résumé

  • DataTable est le composant unique ; passez getTableConfig, getTableActions, et getFormConfig si nécessaire.
  • getTableConfig définit le comportement de la table et les colonnes.
  • getTableActions branche vos Server Actions ou vos appels API.
  • En App Router Next.js, utilisez NuqsAdapter, fournissez un QueryClientProvider partagé, et rendez DataTable depuis un Client Component si vous passez des fonctions.

Voir aussi :

On this page