Column Management — visibility, order, pinning ও sizing
columnVisibility, columnOrder, columnPinning (sticky কলাম), columnSizing ও resize mode, localStorage-এ সংরক্ষণ — বাংলায়।
একটা ভালো data table-এ কলাম ব্যবহারকারী নিজের মতো সাজাতে পারে — লুকাতে পারে, আগে-পিছে বদলাতে পারে, প্রস্থ টানতে পারে। এই চারটি state TanStack-এ আলাদা আলাদা, কিন্তু কাজ একসাথে করে।
চারটি state, এক look-এ
import { useState } from "react";
import type {
ColumnOrderState,
ColumnPinningState,
ColumnSizingState,
VisibilityState,
} from "@tanstack/react-table";
const [columnVisibility, setColumnVisibility] =
useState<VisibilityState>({});
const [columnOrder, setColumnOrder] =
useState<ColumnOrderState>([]);
const [columnPinning, setColumnPinning] =
useState<ColumnPinningState>({ left: [], right: [] });
const [columnSizing, setColumnSizing] =
useState<ColumnSizingState>({});| state | আকার | কী করে |
|---|---|---|
columnVisibility | { price: false } | কলাম লুকায়/আনে |
columnOrder | ["name", "price", …] | কলামের ক্রম বদলায় |
columnPinning | { left: ["select"], right: ["actions"] } | কলাম পাশে আটকায় |
columnSizing | { price: 180 } | কলামের প্রস্থ |
Column visibility
// নির্দিষ্ট কলাম লুকানো
table.getColumn("createdAt")?.toggleVisibility(false);
table.getColumn("createdAt")?.setVisibility(true);
table.getColumn("createdAt")?.getIsVisible(); // boolean
// সব ফিরিয়ে আনা
table.resetColumnVisibility();
table.getVisibleLeafColumns().length;
table.getAllLeafColumns().length;
table.getCenterVisibleLeafColumns().length;লুকানো কলাম row.getVisibleCells()-এ আসেই না — তাই rendering কোডে কোনো শর্ত লেখতে হয় না। এটাই headless আর্কিটেকচারের সবচেয়ে বড় সুবিধা: feature যোগ হলেও markup অপরিবর্তিত থাকে।
Column visibility মেনু
import { Checkbox } from "@/components/ui/checkbox";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function ColumnVisibility({
table,
}: {
table: Table<Product>;
}) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<Settings2 className="mr-2 size-4" />
কলাম
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{table
.getAllLeafColumns()
.filter((column) => column.getCanHide())
.map((column) => (
<DropdownMenuCheckboxItem
key={column.id}
checked={column.getIsVisible()}
onCheckedChange={(checked) =>
column.toggleVisibility(checked === true)
}
>
{columnLabel(column.id)}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}getCanHide() false হলে (checkbox কলাম, actions কলাম) তালিকায় আসে না — ঐপ্রিন্স ছাড়া লুকিয়ে দেওয়ার কোনো কারণ নেই।
Column order — drag & drop
// ম্যানুয়ালি সাজানো
table.setColumnOrder(["select", "name", "price", "actions"]);
table.getState().columnOrder;
// একটি কলাম সরানো
const order = table.getState().columnOrder;
const next = order.filter((id) => id !== "price");
next.splice(2, 0, "price");
table.setColumnOrder(next);
// ডিফল্ট ক্রমে ফেরা
table.resetColumnOrder();columnOrder খালি ([]) রাখলে columns-এর লেখা ক্রমই ব্যবহার হয়। তাই সাধারণত শুধু user কিছু সরালে অ্যারে বসান, না সরালে ফাঁকা রাখুন — তবে "রিসেট" বাটনে resetColumnOrder() দিন।
Column pinning — sticky কলাম
বড় টেবিলে পাশে স্ক্রল করলে actions কলামটা চলে যায় — ব্যবহারকারীর জন্য অস্বস্তি। Pin করলে সেটা বসে থাকে।
// বাঁয়ে / ডানে আটকানো
table.getColumn("actions")?.pin("right");
table.getColumn("name")?.pin("left");
table.getColumn("actions")?.pin(); // unpin (কোনো দিকেও নয়)
table.getColumn("actions")?.getIsPinned(); // false | "left" | "right"
table.getColumn("actions")?.getIsLastColumn();
table.getColumn("actions")?.getIsFirstColumn();
// state দেখা
table.getState().columnPinning;
// { left: ["select", "name"], right: ["actions"] }TanStack flexible header group বানায় — pinned কলাম আলাদা <th> পায়, তাই position: sticky বা shadcn-এর ঐ class দিতে হয়।
Sticky কলাম CSS
// রেন্ডারে পাশের কলামে sticky class
<TableHead
className={cn(
"bg-background",
header.column.getIsLastColumn() && "sticky right-0 z-20",
header.column.getIsPinned() && "shadow-[inset_-1px_0_0_0_currentColor]",
)}
>// body cell-এও একই কাজ
<TableCell
className={cn(
"bg-background",
cell.column.getIsLastColumn() && "sticky right-0 z-10",
)}
>
…
</TableCell>Sticky কলামে background অবশ্যই দিতে হবে, আর খারাপ হলে bg-muted। না দিলে কনটেন্ট উপর দিয়ে ভেসে যায় আর পড়া যায় না। z-index দুই ধাপ — header-এর z-20, cell-এর z-10, যাতে header সবসময় উপরে থাকে।
Pin লেখা / unpin লেখা
<DropdownMenuSub>
<DropdownMenuSubTrigger>
{cell.column.getIsPinned() ? "আটকানো আছে" : "আটকানো নেই"}
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => cell.column.pin("left")}>
<PinIcon className="mr-2 size-4" />
বাঁয়ে আটকান
</DropdownMenuItem>
<DropdownMenuItem onClick={() => cell.column.pin("right")}>
<PinIcon className="mr-2 size-4 rotate-180" />
ডানে আটকান
</DropdownMenuItem>
{cell.column.getIsPinned() && (
<DropdownMenuItem onClick={() => cell.column.pin(false)}>
<PinOffIcon className="mr-2 size-4" />
আটকানো সরান
</DropdownMenuItem>
)}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>Column sizing — প্রস্থ
// ডিফল্ট প্রস্থ (row model অনুযায়ী)
columnSizing: {
minSize: 20, // সর্বনিম্ন ২০px
maxSize: 1000, // সর্বোচ্চ ১০০০px
}
// প্রতি কলামে ডিফল্ট
{ accessorKey: "name", size: 280, minSize: 150, maxSize: 600 }
// resize mode
columnResizeMode: "onChange", // টানা গ্লাইস মুভ হচ্ছে (default)
columnResizeMode: "onEnd", // ছাড়লে হিসাব হয় — বেশি smooth
// state
column.getSize() // বর্তমান px
column.setSize(220)
column.getIsResizing()
table.setColumnSizing({ price: 220 })
table.resetColumnSizing();onEnd মোডে হিসাব শুধু ছেড়ে দিলে হয়, তাই বড় টেবিলে এটা বেশি performant। onChange মোডে প্রতিটি মাউস মুভে layout recalc হয়।
Resize handle
// শুধু ঐ কলামের পাশে একটা পাতলা handle
export function ResizeHandle({
header,
}: {
header: Header<unknown, unknown>;
}) {
const { column } = header;
const isResizing = column.getIsResizing();
return (
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
role="separator"
aria-orientation="vertical"
aria-label={`${column.id} প্রস্থ বদলান`}
className={cn(
"absolute top-0 right-0 h-full w-1 cursor-col-resize touch-none select-none",
"after:absolute after:right-0 after:h-full after:w-1",
isResizing ? "after:bg-primary" : "after:bg-transparent",
"hover:after:bg-primary/50",
)}
style={{
left: isPinning(column.id) ? `${column.getSize()}px` : undefined,
right: isPinnedRight(column.id) ? `-${column.getSize()}px` : undefined,
}}
/>
);
}// TableHead-এর মধ্যে
export function TableHead({ children, column }: HeaderCellProps) {
return (
<div className="relative">
{children}
{column.getCanResize() && <ResizeHandle column={column} />}
</div>
);
}shadcn-এর TableHead শুধু th রেন্ডার করে, ভেতরে div বসানো যায় না। handle বসাতে চাইলে columnResizeMode ব্যবহার না করে নিজে th-এর onMouseDown ধরে column.setSize কল করাও সহজ — header.getResizeHandler() সেটাই করে, শুধু wrapper দরকার হলে display: table-cell ব্যবহার করুন।
একসাথে সব — সংরক্ষণ করে রাখা
এই চারটি preference ইউজারের। ব্রাউজার রিফ্রেশেও থাকা উচিত — নয়তো প্রতিবার সব লুকানো অবস্থা হারিয়ে যাবে।
// localStorage-এ সংরক্ষণ
const STORAGE_KEY = "products-table-layout";
function loadLayout() {
if (typeof window === "undefined") return {};
try {
return JSON.parse(
localStorage.getItem(STORAGE_KEY) ?? "{}",
) as TableLayout;
} catch {
return {};
}
}
type TableLayout = {
columnVisibility?: VisibilityState;
columnOrder?: ColumnOrderState;
columnPinning?: ColumnPinningState;
columnSizing?: ColumnSizingState;
};
// component-এ
const [layout, setLayout] = useState<TableLayout>(loadLayout);
// বদলালে সংরক্ষণ
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(layout));
}, [layout]);// একসাথে save ও load
const table = useReactTable({
// …
onColumnVisibilityChange: setLayout1,
onColumnOrderChange: setLayout2,
onColumnPinningChange: setLayout3,
onColumnSizingChange: setLayout4,
state: {
columnVisibility: layout.columnVisibility,
columnOrder: layout.columnOrder,
columnPinning: layout.columnPinning,
columnSizing: layout.columnSizing,
},
});state option-এ খালি ফাঁকা মান ({}, []) দিলে initialState কাজ করে। শুধু table বানিয়ে getColumn("price").hide() বললে localStorage থেকে আসা preference উড়ে যাবে।
রিসেট বাটন
function resetLayout(table: Table<Product>) {
table.resetColumnVisibility();
table.resetColumnOrder();
table.resetColumnPinning();
table.resetColumnSizing();
}
// ডিফল্ট কিন্তু পরিবর্তিত কোনো কিছু আছে কিনা
const isDirty =
Object.keys(table.getState().columnVisibility).length > 0 ||
table.getState().columnOrder.length > 0 ||
table.getState().columnPinning.left.length > 0 ||
table.getState().columnPinning.right.length > 0 ||
Object.keys(table.getState().columnSizing).length > 0;মোবাইলে ধারণা
ছোট স্ক্রিনে একই টেবিল রাখলে কলামের চেহারা নষ্ট হয়। দুটি সাধারণ সমাধান:
// ১. default visibility অনুযায়ী শুধু ২-৩টি কলাম রাখুন
const [columnVisibility, setColumnVisibility] = useState({
select: true,
name: true,
price: true,
stock: false,
createdAt: false,
});
// বড় স্ক্রিনে
useEffect(() => {
if (window.innerWidth >= 768) setColumnVisibility({});
}, []);
// ২. মোবাইলে পাশ থেকে স্লাইড করে কলাম দেখান
<Sheet>
<SheetTrigger asChild>
<Button size="sm" variant="outline">কলাম দেখুন</Button>
</SheetTrigger>
<SheetContent side="right" className="w-72">
{/* একই ColumnVisibility মেনু */}
</SheetContent>
</Sheet>সাধারণ বাগ
| ঘটনা | কারণ | সমাধান |
|---|---|---|
| লুকানো কলামের জায়গা ফাঁকা | getAllCells() ব্যবহার | getVisibleCells() ব্যবহার করুন |
| sticky কলামের উপর লেখা ভেসে যায় | background নেই | bg-background দিন |
| Resize করলে কলাম ফিরে আসে | state দেওয়া হয়নি | state: { columnSizing } |
| localStorage পড়া যায় না | SSR-এ window নেই | typeof window check |
| menu-তে "actions" দেখাচ্ছে | enableHiding: false নেই | দুটোই false দিন |
| pin করলে কলাম উলটো | state-এ left/right ভুল | key ঠিক "left" / "right" |
| resize করলে সব সরে যায় | resize handle th-র বাইরে | position: relative দিন |
সারসংক্ষেপ
- চারটি স্বাধীন state:
columnVisibility,columnOrder,columnPinning,columnSizing - লুকালে
getVisibleCells()আর render কোড অপরিবর্তিত থাকে - sticky-র জন্য
bg-background+z-indexবাধ্যতামূলক columnResizeMode: "onEnd"বড় টেবিলে দ্রুত- preference localStorage-এ রাখলে UX অনেক ভালো
পরবর্তী
এতক্ষণ সব হিসাব browser-এই হচ্ছিল। পরের অধ্যায়ে server-side mode — manual flag, rowCount, আর TanStack Query-এর সাথে সেতু বানানো।