Skip to content

jacksonkasi1/FlexiSheet

Repository files navigation

FlexiSheet

FlexiSheet is a powerful, reusable table component for React applications. It supports features like editable cells, row/column disabling, Zod-based validation, grouping rows by headers, and configurable footers.


Table of Contents

  1. Features
  2. Demo
  3. Installation
  4. Basic Usage
  5. Advanced Options
  6. FAQ
  7. Development
  8. License

Features

  • Editable Cells: Supports real-time editing with validation.
  • Zod Validation: Per-column validation using Zod schemas.
  • Row/Column Disabling: Disable specific rows or columns.
  • Grouping Rows: Group data using a headerKey field.
  • Footer Support: Add totals rows and custom footer elements.

Demo

Link: https://flexisheet.vercel.app/

FlexiSheet Demo


Installation

Prerequisites

Ensure you have the following installed in your project:

  1. Zod for validation:

    bun install zod
  2. TanStack Table for table functionality:

    bun install @tanstack/react-table
  3. ShadCN/UI for UI components:

    bunx --bun shadcn@latest add table
  4. Tailwind CSS for styling:

    bun install tailwindcss postcss autoprefixer

Basic Usage

1. Define Your Data

👀 NOTE: The id field is required for each row. It should be unique for each row.

const initialData = [
  { id: 1, materialName: "Material A", cft: 0.1, rate: 100, amount: 10 },
  { id: 2, materialName: "Material B", cft: 0.2, rate: 200, amount: 40 },
];

2. Define Column Schema with Validation

import { z } from "zod";

const materialNameSchema = z.string().min(1, "Required");
const cftSchema = z.number().nonnegative().optional();
const rateSchema = z.number().min(0, "Must be >= 0");
const amountSchema = z.number().min(0, "Must be >= 0");

const columns = [
  { accessorKey: "materialName", header: "Material Name", validationSchema: materialNameSchema },
  { accessorKey: "cft", header: "CFT", validationSchema: cftSchema },
  { accessorKey: "rate", header: "Rate", validationSchema: rateSchema },
  { accessorKey: "amount", header: "Amount", validationSchema: amountSchema },
];

3. Render the Table

import React, { useState } from "react";
import SheetTable from "./components/sheet-table";

const App = () => {
  const [data, setData] = useState(initialData);

  /**
   * onEdit callback: updates local state if the new value is valid. (Normal usage)
   */
  const handleEdit = <K extends keyof RowData>(
    rowId: string, // Unique identifier for the row
    columnId: K, // Column key
    value: RowData[K], // New value for the cell
  ) => {
    setData((prevData) =>
      prevData.map(
        (row) =>
          String(row.id) === rowId
            ? { ...row, [columnId]: value } // Update the row if the ID matches
            : row, // Otherwise, return the row unchanged
      ),
    );

    console.log(
      `State updated [row id=${rowId}, column=${columnId}, value=${value}]`,
      value,
    );
  };

  return (
    <SheetTable
      columns={columns}
      data={data}
      onEdit={handleEdit}
      disabledColumns={["amount"]} // Example: Disable editing for "amount" col
      showHeader={true}
    />
  );
};

export default App;

Advanced Options

Grouped Rows Example

const groupedData = [
  {
    id: 1,
    headerKey: "Group A",
    materialName: "Material A",
    cft: 0.1,
    rate: 100,
    amount: 10,
  },
  {
    id: 2,
    headerKey: "Group A",
    materialName: "Material B",
    cft: 0.2,
    rate: 200,
    amount: 40,
  },
  {
    id: 3,
    headerKey: "Group B",
    materialName: "Material C",
    cft: 0.3,
    rate: 300,
    amount: 90,
  },
];

Group Specific Disabled Rows

<SheetTable
  columns={columns}
  data={groupedData}
  disabledColumns={["materialName"]}
  disabledRows={{
    "Dipping - 2 times": [0], // Disable the second row in this group
    Spraying: [1], // Disable the first row in this group
  }}
/>

Footer Example

<SheetTable
  columns={columns}
  data={data}
  totalRowValues={{ cft: 0.6, rate: 600, amount: 140 }}
  totalRowLabel="Total"
  totalRowTitle="Summary"
  footerElement={<div>Custom Footer Content</div>}
/>

FAQ

1. How do I disable editing for specific columns or rows?

You can disable specific rows and columns by using the disabledColumns and disabledRows props in the SheetTable component.

  • Disable Columns:

    <SheetTable
      disabledColumns={["amount", "rate"]} // Disable editing for "amount" and "rate" columns
    />
  • Disable Rows(normal):

    <SheetTable
      disabledRows={[0, 1]} // Disable the 1st & 2nd row
    />
  • Disable Rows(group):

    <SheetTable
      disabledRows={{
        "Group A": [0], // Disable the first row in Group A
        "Group B": [1], // Disable the second row in Group B
      }}
    />

2. Can I add custom validation for columns?

Yes, you can use Zod schemas to define validation rules for each column using the validationSchema property.

Example:

const rateSchema = z.number().min(0, "Rate must be greater than or equal to 0");
const columns = [
  {
    accessorKey: "rate",
    header: "Rate",
    validationSchema: rateSchema,
  },
];

3. What happens if validation fails?

If validation fails while editing a cell, the cell will:

  • Display an error class (e.g., bg-destructive/25 by default).
  • Not trigger the onEdit callback until the value is valid.

4. How do I group rows?

To group rows, provide a headerKey field in your data and the SheetTable will automatically group rows based on this key.

Example:

const groupedData = [
  { headerKey: "Group A", materialName: "Material A", cft: 0.1 },
  { headerKey: "Group B", materialName: "Material B", cft: 0.2 },
];

5. Can I dynamically resize columns?

Yes, you can enable column resizing by setting enableColumnSizing to true and providing column size properties (size, minSize, and maxSize) in the column definitions.

Example:

const columns = [
  {
    accessorKey: "materialName",
    header: "Material Name",
    size: 200,
    minSize: 100,
    maxSize: 300,
  },
];
<SheetTable columns={columns} enableColumnSizing={true} />;

6. How do I add a footer with totals or custom elements?

Use the totalRowValues, totalRowLabel, and footerElement props to define footer content.

Example:

<SheetTable
  totalRowValues={{ cft: 0.6, rate: 600, amount: 140 }}
  totalRowLabel="Total"
  footerElement={<div>Custom Footer Content</div>}
/>

7. Does FlexiSheet support large datasets?

Yes, but for optimal performance:

  • Use memoization for columns and data to prevent unnecessary re-renders.
  • Consider integrating virtualization (e.g., react-window) for very large datasets.

8. Can I hide columns dynamically?

Yes, you can control column visibility using the tableOptions.initialState.columnVisibility configuration.

Example:

<SheetTable
  tableOptions={{
    initialState: { columnVisibility: { amount: false } }, // Hide "amount" column
  }}
/>

9. How do I handle user actions like copy/paste or undo?

FlexiSheet supports common keyboard actions like copy (Ctrl+C), paste (Ctrl+V), and undo (Ctrl+Z). You don’t need to configure anything to enable these actions.


10. How do I validate the entire table before submission?

Use Zod's array schema to validate the entire dataset on form submission.

Example:

const handleSubmit = () => {
  const tableSchema = z.array(rowDataZodSchema);
  const result = tableSchema.safeParse(data);
  if (!result.success) {
    console.error("Invalid data:", result.error.issues);
  } else {
    console.log("Valid data:", data);
  }
};

11. How does the sub-row data structure look, and how can I handle sub-row editing?

Sub-rows are supported using a subRows field within each row object. The subRows field is an array of child rows, where each child row can have its own data and even further sub-rows (nested structure).

Example Sub-row Data Structure:

const dataWithSubRows = [
  {
    id: 1,
    materialName: "Material A",
    cft: 0.1,
    rate: 100,
    amount: 10,
    subRows: [
      {
        id: 1.1,
        materialName: "Sub-Material A1",
        cft: 0.05,
        rate: 50,
        amount: 5,
      },
      {
        id: 1.2,
        materialName: "Sub-Material A2",
        cft: 0.05,
        rate: 50,
        amount: 5,
      },
    ],
  },
  {
    id: 2,
    materialName: "Material B",
    cft: 0.2,
    rate: 200,
    amount: 40,
  },
];

How to Handle Sub-row Editing:

To handle editing for sub-rows, ensure that your onEdit callback can traverse the subRows array and update the appropriate row.

Example:

function updateNestedRow<K extends keyof RowData>(
  rows: RowData[],
  rowId: string,
  colKey: K,
  newValue: RowData[K],
): RowData[] {
  return rows.map((row) => {
    if (row.id === rowId) {
      return { ...row, [colKey]: newValue };
    }
    if (row.subRows && row.subRows.length > 0) {
      return {
        ...row,
        subRows: updateNestedRow(row.subRows, rowId, colKey, newValue),
      };
    }
    return row;
  });
}

export default function HomePage() {
  const [data, setData] = useState<RowData[]>(initialData);

  const handleEdit = <K extends keyof RowData>(
    rowId: string,
    columnId: K,
    value: RowData[K],
  ) => {
    setData((prevData) => {
      const newRows = updateNestedRow(prevData, rowId, columnId, value);
      return newRows;
    });
  };
}

Development

  1. Clone the repository:

    git clone https://github.com/jacksonkasi1/FlexiSheet.git
  2. Install dependencies:

    bun install
  3. Run the development server:

    bun dev

License

This project is licensed under the MIT License. See the LICENSE file for details.