Documentation
Column Visibility
Column visibility lets users show or hide columns through the column editor. They can search, drag to reorder, and toggle which columns appear in the grid.
1import {2 SimpleTable,3 type ColumnEditorRowRendererProps,4 type ColumnVisibilityState,5 type Theme,6} from "@simple-table/react";7import { useMemo, useCallback } from "react";8import {9 columnVisibilityConfig,10 getColumnVisibilityDemoHeaders,11 loadColumnVisibilityDemoSaved,12 saveColumnVisibilityDemoState,13} from "./column-visibility.demo-data";14import "@simple-table/react/styles.css";1516const ColumnVisibilityDemo = ({17 height = "400px",18 theme,19}: {20 height?: string | number;21 theme?: Theme;22}) => {23 const headers = useMemo(24 () => getColumnVisibilityDemoHeaders(loadColumnVisibilityDemoSaved()),25 [],26 );2728 const onColumnVisibilityChange = useCallback((state: ColumnVisibilityState) => {29 saveColumnVisibilityDemoState(state);30 }, []);3132 return (33 <SimpleTable34 columns={headers}35 rows={columnVisibilityConfig.rows}36 enableColumnEditor37 enableColumnEditorInitOpen38 height={height}39 theme={theme}40 onColumnVisibilityChange={onColumnVisibilityChange}41 columnEditorConfig={{42 ...columnVisibilityConfig.tableProps.columnEditorConfig,43 rowRenderer: ({ components }: ColumnEditorRowRendererProps) => (44 <div45 style={{46 width: "100%",47 display: "flex",48 alignItems: "center",49 justifyContent: "space-between",50 gap: "8px",51 paddingRight: "8px",52 }}53 >54 <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>55 {components?.expandIcon}56 {components?.checkbox}57 {components?.labelContent}58 </div>59 <div>{components?.dragIcon}</div>60 </div>61 ),62 }}63 />64 );65};6667export default ColumnVisibilityDemo;
1<script setup lang="ts">2import {SimpleTable} from "@simple-table/vue";import type { ColumnVisibilityState, Theme } from "@simple-table/vue";3import MarketingColumnEditorRow from "./MarketingColumnEditorRow.vue";4import {5 columnVisibilityConfig,6 getColumnVisibilityDemoHeaders,7 loadColumnVisibilityDemoSaved,8 saveColumnVisibilityDemoState,9} from "./column-visibility.demo-data";10import "@simple-table/vue/styles.css";1112const props = withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {13 height: "400px",14});1516const columns = getColumnVisibilityDemoHeaders(loadColumnVisibilityDemoSaved());1718const columnEditorConfig = {19 ...columnVisibilityConfig.tableProps.columnEditorConfig,20 rowRenderer: MarketingColumnEditorRow,21};2223function onColumnVisibilityChange(state: ColumnVisibilityState) {24 saveColumnVisibilityDemoState(state);25}26</script>2728<template>29 <SimpleTable30 :columns="columns"31 :rows="columnVisibilityConfig.rows"32 :enable-column-editor="columnVisibilityConfig.tableProps.enableColumnEditor"33 :enable-column-editor-init-open="columnVisibilityConfig.tableProps.enableColumnEditorInitOpen"34 :column-editor-config="columnEditorConfig"35 :height="props.height"36 :theme="props.theme"37 :on-column-visibility-change="onColumnVisibilityChange"38 />39</template>404142// MarketingColumnEditorRow.vue43<script lang="ts">44import { defineComponent, h } from "vue";45import type { ColumnEditorRowRendererProps } from "@simple-table/vue";4647export default defineComponent({48 name: "MarketingColumnEditorRow",49 props: {50 header: { type: Object, required: true },51 components: { type: Object, required: true },52 },53 setup(props: ColumnEditorRowRendererProps) {54 return () =>55 h(56 "div",57 {58 style: {59 width: "100%",60 display: "flex",61 alignItems: "center",62 justifyContent: "space-between",63 gap: "8px",64 paddingRight: "8px",65 },66 },67 [68 h(69 "div",70 {71 style: {72 display: "flex",73 alignItems: "center",74 gap: "8px",75 },76 },77 [78 props.components.expandIcon,79 props.components.checkbox,80 props.components.labelContent,81 ].filter((v) => v != null && v !== false),82 ),83 h(84 "div",85 { style: { display: "flex", alignItems: "center" } },86 [props.components.dragIcon].filter((v) => v != null && v !== false),87 ),88 ],89 );90 },91});92</script>93
1import { Component, Input } from "@angular/core";2import {SimpleTableComponent} from "@simple-table/angular";import type { AngularColumnDef, ColumnVisibilityState, Row, Theme } from "@simple-table/angular";3import { columnVisibilityConfig, getColumnVisibilityDemoHeaders, loadColumnVisibilityDemoSaved, saveColumnVisibilityDemoState } from "./column-visibility.demo-data";4import { MarketingColumnEditorRowComponent } from "./marketing-column-editor-row.component";5import "@simple-table/angular/styles.css";67@Component({8 selector: "column-visibility-demo",9 standalone: true,10 imports: [SimpleTableComponent],11 template: `12 <simple-table13 [rows]="rows"14 [columns]="headers"15 [height]="height"16 [theme]="theme"17 [enableColumnEditor]="tableProps.enableColumnEditor"18 [enableColumnEditorInitOpen]="tableProps.enableColumnEditorInitOpen"19 [columnEditorConfig]="columnEditorConfig"20 [onColumnVisibilityChange]="onVisibilityChange"21 ></simple-table>22 `,23})24export class ColumnVisibilityDemoComponent {25 @Input() height: string | number = "400px";26 @Input() theme?: Theme;2728 readonly rows: Row[] = columnVisibilityConfig.rows;29 readonly headers: AngularColumnDef[] = getColumnVisibilityDemoHeaders(30 loadColumnVisibilityDemoSaved(),31 );32 readonly tableProps = columnVisibilityConfig.tableProps;33 readonly columnEditorConfig = {34 ...columnVisibilityConfig.tableProps.columnEditorConfig,35 rowRenderer: MarketingColumnEditorRowComponent,36 };3738 readonly onVisibilityChange = (state: ColumnVisibilityState) => {39 saveColumnVisibilityDemoState(state);40 };41}424344// column-visibility.demo-data.ts45// Self-contained demo table setup for this example (aligned with simple-table-marketing column visibility demo).46import type { ColumnVisibilityState, AngularColumnDef, Row } from "@simple-table/angular";4748export const COLUMN_VISIBILITY_DEMO_STORAGE_KEY = "columnVisibilityDemo";4950export function loadColumnVisibilityDemoSaved(): ColumnVisibilityState {51 if (typeof window === "undefined") return {};52 try {53 const saved = localStorage.getItem(COLUMN_VISIBILITY_DEMO_STORAGE_KEY);54 return saved ? JSON.parse(saved) : {};55 } catch {56 return {};57 }58}5960export function saveColumnVisibilityDemoState(state: ColumnVisibilityState): void {61 if (typeof window === "undefined") return;62 try {63 localStorage.setItem(COLUMN_VISIBILITY_DEMO_STORAGE_KEY, JSON.stringify(state));64 } catch {65 /* ignore */66 }67}6869export const columnVisibilityData: Row[] = [70 { id: 1, firstName: "Alice", lastName: "Johnson", email: "alice@example.com", phone: "555-0101", role: "Engineer", department: "Engineering", location: "NYC", startDate: "2021-03-15" },71 { id: 2, firstName: "Bob", lastName: "Martinez", email: "bob@example.com", phone: "555-0102", role: "Designer", department: "Design", location: "LA", startDate: "2022-07-22" },72 { id: 3, firstName: "Clara", lastName: "Chen", email: "clara@example.com", phone: "555-0103", role: "PM", department: "Product", location: "SF", startDate: "2020-01-10" },73 { id: 4, firstName: "David", lastName: "Kim", email: "david@example.com", phone: "555-0104", role: "Engineer", department: "Engineering", location: "CHI", startDate: "2019-11-05" },74 { id: 5, firstName: "Elena", lastName: "Rossi", email: "elena@example.com", phone: "555-0105", role: "Analyst", department: "Analytics", location: "BOS", startDate: "2023-02-14" },75 { id: 6, firstName: "Frank", lastName: "Müller", email: "frank@example.com", phone: "555-0106", role: "Engineer", department: "Engineering", location: "SEA", startDate: "2021-09-30" },76 { id: 7, firstName: "Grace", lastName: "Park", email: "grace@example.com", phone: "555-0107", role: "Designer", department: "Design", location: "AUS", startDate: "2022-04-18" },77 { id: 8, firstName: "Henry", lastName: "Patel", email: "henry@example.com", phone: "555-0108", role: "Lead", department: "Engineering", location: "DEN", startDate: "2018-05-20" },78];7980export const columnVisibilityHeaders: AngularColumnDef[] = [81 { accessor: "id", label: "ID", width: 60, type: "number" },82 { accessor: "firstName", label: "First Name", width: 120, type: "string" },83 { accessor: "lastName", label: "Last Name", width: 120, type: "string" },84 { accessor: "email", label: "Email", width: 200, type: "string" },85 { accessor: "phone", label: "Phone", width: 120, type: "string", hide: true },86 { accessor: "role", label: "Role", width: 130, type: "string" },87 { accessor: "department", label: "Department", width: 140, type: "string" },88 { accessor: "location", label: "Location", width: 100, type: "string", hide: true },89 {90 accessor: "startDate",91 label: "Start Date",92 width: 130,93 type: "date",94 valueFormatter: ({ value }) => new Date(value as string).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }),95 },96];9798export function getColumnVisibilityDemoHeaders(99 savedVisibility: ColumnVisibilityState = loadColumnVisibilityDemoSaved(),100): AngularColumnDef[] {101 return columnVisibilityHeaders.map((header) => ({102 ...header,103 hide:104 savedVisibility[header.accessor] === false ||105 (savedVisibility[header.accessor] === undefined && header.accessor === "email") ||106 (savedVisibility[header.accessor] === undefined && header.hide === true),107 }));108}109110export const columnVisibilityConfig = {111 headers: columnVisibilityHeaders,112 rows: columnVisibilityData,113 tableProps: {114 enableColumnEditor: true as const,115 enableColumnEditorInitOpen: true as const,116 columnEditorConfig: {117 text: "Manage Columns",118 searchEnabled: true,119 searchPlaceholder: "Search columns…",120 },121 },122} as const;123124125// marketing-column-editor-row.component.ts126import {127 AfterViewInit,128 Component,129 ElementRef,130 Input,131 OnChanges,132 ViewChild,133} from "@angular/core";134import type { ColumnEditorRowRendererProps } from "@simple-table/angular";135136function attach(slot: unknown, host: HTMLElement | undefined): void {137 if (!host) return;138 host.replaceChildren();139 if (slot == null) return;140 if (typeof slot === "string") {141 host.textContent = slot;142 } else if (slot instanceof Node) {143 host.appendChild(slot);144 }145}146147/** Marketing-style column-editor row; slots are framework-provided nodes or strings. */148@Component({149 selector: "st-examples-marketing-column-editor-row",150 standalone: true,151 template: `152 <div153 style="width:100%;display:flex;align-items:center;justify-content:space-between;gap:8px;padding-right:8px;"154 >155 <div style="display:flex;align-items:center;gap:8px;">156 <span #expandHost></span>157 <span #checkboxHost></span>158 <span #labelHost></span>159 </div>160 <span #dragHost></span>161 </div>162 `,163})164export class MarketingColumnEditorRowComponent implements AfterViewInit, OnChanges {165 @Input({ required: true }) accessor!: ColumnEditorRowRendererProps["accessor"];166 @Input({ required: true }) header!: ColumnEditorRowRendererProps["header"];167 @Input({ required: true }) components!: ColumnEditorRowRendererProps["components"];168 @Input() panelSection?: ColumnEditorRowRendererProps["panelSection"];169 @Input() essential?: ColumnEditorRowRendererProps["essential"];170 @Input() canToggleVisibility?: ColumnEditorRowRendererProps["canToggleVisibility"];171 @Input() allowColumnPinning?: ColumnEditorRowRendererProps["allowColumnPinning"];172 @Input() pinControl?: ColumnEditorRowRendererProps["pinControl"];173174 @ViewChild("expandHost") expandRef?: ElementRef<HTMLSpanElement>;175 @ViewChild("checkboxHost") checkboxRef?: ElementRef<HTMLSpanElement>;176 @ViewChild("labelHost") labelRef?: ElementRef<HTMLSpanElement>;177 @ViewChild("dragHost") dragRef?: ElementRef<HTMLSpanElement>;178179 ngAfterViewInit(): void {180 this.syncSlots();181 }182183 ngOnChanges(): void {184 this.syncSlots();185 }186187 private syncSlots(): void {188 const c = this.components;189 attach(c.expandIcon, this.expandRef?.nativeElement);190 attach(c.checkbox, this.checkboxRef?.nativeElement);191 attach(c.labelContent, this.labelRef?.nativeElement);192 attach(c.dragIcon, this.dragRef?.nativeElement);193 }194}195
1<script lang="ts">2 import {SimpleTable} from "@simple-table/svelte"; import type { Theme } from "@simple-table/svelte";3 import MarketingColumnEditorRow from "./MarketingColumnEditorRow.svelte";4 import { columnVisibilityConfig } from "./column-visibility.demo-data";5 import "@simple-table/svelte/styles.css";67 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();89 const columns = columnVisibilityConfig.headers;1011 const columnEditorConfig = {12 ...columnVisibilityConfig.tableProps.columnEditorConfig,13 rowRenderer: MarketingColumnEditorRow,14 };15</script>1617<SimpleTable18 {columns}19 rows={columnVisibilityConfig.rows}20 enableColumnEditor={columnVisibilityConfig.tableProps.enableColumnEditor}21 enableColumnEditorInitOpen={columnVisibilityConfig.tableProps.enableColumnEditorInitOpen}22 columnEditorConfig={columnEditorConfig}23 {height}24 {theme}25/>262728// MarketingColumnEditorRow.svelte29<script lang="ts">30 import type { ColumnEditorRowRendererProps } from "@simple-table/svelte";3132 let { components }: ColumnEditorRowRendererProps = $props();3334 let expandHost: HTMLSpanElement | undefined = $state();35 let checkboxHost: HTMLSpanElement | undefined = $state();36 let labelHost: HTMLSpanElement | undefined = $state();37 let dragHost: HTMLSpanElement | undefined = $state();3839 function attach(slot: unknown, host: HTMLElement | undefined): void {40 if (!host) return;41 host.replaceChildren();42 if (slot == null) return;43 if (typeof slot === "string") {44 host.textContent = slot;45 } else if (slot instanceof Node) {46 host.appendChild(slot);47 }48 }4950 $effect(() => {51 attach(components.expandIcon, expandHost);52 });53 $effect(() => {54 attach(components.checkbox, checkboxHost);55 });56 $effect(() => {57 attach(components.labelContent, labelHost);58 });59 $effect(() => {60 attach(components.dragIcon, dragHost);61 });62</script>6364<div65 style="width:100%;display:flex;align-items:center;justify-content:space-between;gap:8px;padding-right:8px;"66>67 <div style="display:flex;align-items:center;gap:8px;">68 <span bind:this={expandHost}></span>69 <span bind:this={checkboxHost}></span>70 <span bind:this={labelHost}></span>71 </div>72 <span bind:this={dragHost}></span>73</div>74
1import {2 SimpleTable,3 type ColumnVisibilityState,4 type Theme,5} from "@simple-table/solid";6import {7 columnVisibilityConfig,8 getColumnVisibilityDemoHeaders,9 loadColumnVisibilityDemoSaved,10 saveColumnVisibilityDemoState,11} from "./column-visibility.demo-data";12import MarketingColumnEditorRow from "./MarketingColumnEditorRow";13import "@simple-table/solid/styles.css";1415export default function ColumnVisibilityDemo(props: { height?: string | number; theme?: Theme }) {16 const headers = () => getColumnVisibilityDemoHeaders(loadColumnVisibilityDemoSaved());1718 const onColumnVisibilityChange = (state: ColumnVisibilityState) => {19 saveColumnVisibilityDemoState(state);20 };2122 return (23 <SimpleTable24 columns={headers()}25 rows={columnVisibilityConfig.rows}26 enableColumnEditor27 enableColumnEditorInitOpen28 height={props.height ?? "400px"}29 theme={props.theme}30 onColumnVisibilityChange={onColumnVisibilityChange}31 columnEditorConfig={{32 ...columnVisibilityConfig.tableProps.columnEditorConfig,33 rowRenderer: MarketingColumnEditorRow,34 }}35 />36 );37}383940// MarketingColumnEditorRow.tsx41import type { ColumnEditorRowRendererProps } from "@simple-table/solid";42import { createEffect } from "solid-js";4344function attach(slot: unknown, host: HTMLElement | undefined): void {45 if (!host) return;46 host.replaceChildren();47 if (slot == null) return;48 if (typeof slot === "string") {49 host.textContent = slot;50 } else if (slot instanceof Node) {51 host.appendChild(slot);52 }53}5455function SlotHost(props: { slot: () => unknown }) {56 let host: HTMLSpanElement | undefined;57 createEffect(() => {58 attach(props.slot(), host);59 });60 return (61 <span62 ref={(el) => {63 host = el;64 attach(props.slot(), el);65 }}66 />67 );68}6970export default function MarketingColumnEditorRow(props: ColumnEditorRowRendererProps) {71 return (72 <div73 style={{74 width: "100%",75 display: "flex",76 "align-items": "center",77 "justify-content": "space-between",78 gap: "8px",79 "padding-right": "8px",80 }}81 >82 <div style={{ display: "flex", "align-items": "center", gap: "8px" }}>83 <SlotHost slot={() => props.components.expandIcon} />84 <SlotHost slot={() => props.components.checkbox} />85 <SlotHost slot={() => props.components.labelContent} />86 </div>87 <SlotHost slot={() => props.components.dragIcon} />88 </div>89 );90}91
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme } from "simple-table-core";3import {4 buildMarketingStyleColumnEditorRowRenderer,5 columnVisibilityConfig,6 getColumnVisibilityDemoHeaders,7 saveColumnVisibilityDemoState,8} from "./column-visibility.demo-data";9import "simple-table-core/styles.css";1011export function renderColumnVisibilityDemo(12 container: HTMLElement,13 options?: { height?: string | number; theme?: Theme },14): SimpleTableVanilla {15 const table = new SimpleTableVanilla(container, {16 columns: getColumnVisibilityDemoHeaders(),17 rows: columnVisibilityConfig.rows,18 height: options?.height ?? "400px",19 theme: options?.theme,20 enableColumnEditor: columnVisibilityConfig.tableProps.enableColumnEditor,21 enableColumnEditorInitOpen: columnVisibilityConfig.tableProps.enableColumnEditorInitOpen,22 onColumnVisibilityChange: saveColumnVisibilityDemoState,23 columnEditorConfig: {24 ...columnVisibilityConfig.tableProps.columnEditorConfig,25 rowRenderer: buildMarketingStyleColumnEditorRowRenderer,26 },27 });28 return table;29}303132// column-visibility.demo-data.ts33// Self-contained demo table setup for this example (aligned with simple-table-marketing column visibility demo).34import type {35 ColumnEditorRowRenderer,36 ColumnEditorRowRendererProps,37 ColumnVisibilityState,38 ColumnDef,39 Row,40} from "simple-table-core";4142export const COLUMN_VISIBILITY_DEMO_STORAGE_KEY = "columnVisibilityDemo";4344export function loadColumnVisibilityDemoSaved(): ColumnVisibilityState {45 if (typeof window === "undefined") return {};46 try {47 const saved = localStorage.getItem(COLUMN_VISIBILITY_DEMO_STORAGE_KEY);48 return saved ? JSON.parse(saved) : {};49 } catch {50 return {};51 }52}5354export function saveColumnVisibilityDemoState(state: ColumnVisibilityState): void {55 if (typeof window === "undefined") return;56 try {57 localStorage.setItem(COLUMN_VISIBILITY_DEMO_STORAGE_KEY, JSON.stringify(state));58 } catch {59 /* ignore */60 }61}6263export const columnVisibilityData: Row[] = [64 { id: 1, firstName: "Alice", lastName: "Johnson", email: "alice@example.com", phone: "555-0101", role: "Engineer", department: "Engineering", location: "NYC", startDate: "2021-03-15" },65 { id: 2, firstName: "Bob", lastName: "Martinez", email: "bob@example.com", phone: "555-0102", role: "Designer", department: "Design", location: "LA", startDate: "2022-07-22" },66 { id: 3, firstName: "Clara", lastName: "Chen", email: "clara@example.com", phone: "555-0103", role: "PM", department: "Product", location: "SF", startDate: "2020-01-10" },67 { id: 4, firstName: "David", lastName: "Kim", email: "david@example.com", phone: "555-0104", role: "Engineer", department: "Engineering", location: "CHI", startDate: "2019-11-05" },68 { id: 5, firstName: "Elena", lastName: "Rossi", email: "elena@example.com", phone: "555-0105", role: "Analyst", department: "Analytics", location: "BOS", startDate: "2023-02-14" },69 { id: 6, firstName: "Frank", lastName: "Müller", email: "frank@example.com", phone: "555-0106", role: "Engineer", department: "Engineering", location: "SEA", startDate: "2021-09-30" },70 { id: 7, firstName: "Grace", lastName: "Park", email: "grace@example.com", phone: "555-0107", role: "Designer", department: "Design", location: "AUS", startDate: "2022-04-18" },71 { id: 8, firstName: "Henry", lastName: "Patel", email: "henry@example.com", phone: "555-0108", role: "Lead", department: "Engineering", location: "DEN", startDate: "2018-05-20" },72];7374export const columnVisibilityHeaders: ColumnDef[] = [75 { accessor: "id", label: "ID", width: 60, type: "number" },76 { accessor: "firstName", label: "First Name", width: 120, type: "string" },77 { accessor: "lastName", label: "Last Name", width: 120, type: "string" },78 { accessor: "email", label: "Email", width: 200, type: "string" },79 { accessor: "phone", label: "Phone", width: 120, type: "string", hide: true },80 { accessor: "role", label: "Role", width: 130, type: "string" },81 { accessor: "department", label: "Department", width: 140, type: "string" },82 { accessor: "location", label: "Location", width: 100, type: "string", hide: true },83 {84 accessor: "startDate",85 label: "Start Date",86 width: 130,87 type: "date",88 valueFormatter: ({ value }) => new Date(value as string).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }),89 },90];9192export function getColumnVisibilityDemoHeaders(93 savedVisibility: ColumnVisibilityState = loadColumnVisibilityDemoSaved(),94): ColumnDef[] {95 return columnVisibilityHeaders.map((header) => ({96 ...header,97 hide:98 savedVisibility[header.accessor] === false ||99 (savedVisibility[header.accessor] === undefined && header.accessor === "email") ||100 (savedVisibility[header.accessor] === undefined && header.hide === true),101 }));102}103104export const columnVisibilityConfig = {105 headers: columnVisibilityHeaders,106 rows: columnVisibilityData,107 tableProps: {108 enableColumnEditor: true as const,109 enableColumnEditorInitOpen: true as const,110 columnEditorConfig: {111 text: "Manage Columns",112 searchEnabled: true,113 searchPlaceholder: "Search columns…",114 },115 },116} as const;117118function appendMarketingColumnEditorSlot(parent: HTMLElement, slot: string | Node | undefined): void {119 if (slot == null) return;120 if (typeof slot === "string") {121 parent.appendChild(document.createTextNode(slot));122 } else {123 parent.appendChild(slot);124 }125}126127/** Vanilla-only copy of the marketing column-editor row layout (not shared with other framework examples). */128export const buildMarketingStyleColumnEditorRowRenderer = (({129 components,130}: ColumnEditorRowRendererProps): HTMLElement => {131 const outer = document.createElement("div");132 outer.style.width = "100%";133 outer.style.display = "flex";134 outer.style.alignItems = "center";135 outer.style.justifyContent = "space-between";136 outer.style.gap = "8px";137 outer.style.paddingRight = "8px";138139 const left = document.createElement("div");140 left.style.display = "flex";141 left.style.alignItems = "center";142 left.style.gap = "8px";143 appendMarketingColumnEditorSlot(left, components.expandIcon as Node | string | undefined);144 appendMarketingColumnEditorSlot(left, components.checkbox as Node | string | undefined);145 appendMarketingColumnEditorSlot(left, components.labelContent as Node | string | undefined);146 outer.appendChild(left);147148 const right = document.createElement("div");149 appendMarketingColumnEditorSlot(right, components.dragIcon as Node | string | undefined);150 outer.appendChild(right);151152 return outer;153}) satisfies ColumnEditorRowRenderer;154
Basic Implementation
Column visibility can be controlled using the hide property in the header objects and the enableColumnEditor prop on the SimpleTable component.
Column Visibility Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
ColumnDef.hideboolean | Optional | Controls the initial visibility of the column. When true, the column will be hidden by default. | |
enableColumnEditorboolean | Optional | Enables the column visibility controls, allowing users to show/hide columns through a UI panel. | |
enableColumnEditorInitOpenboolean | Optional | Opens the column visibility menu by default when the table loads. Requires enableColumnEditor to be true. | |
columnEditorConfig.showToggleboolean | Optional | When false, hides the built-in vertical Columns strip while keeping the editor. Open it from your own UI with tableRef.current.toggleColumnEditor(). Default: true. | |
onColumnVisibilityChange(visibilityState: ColumnVisibilityState) => void | Optional | Callback triggered when column visibility changes. Receives a ColumnVisibilityState object mapping each column accessor to its visibility state (true = visible, false = hidden). Perfect for persisting user preferences or syncing visibility state with external storage. | |
ColumnDef.excludeFromRenderboolean | Optional | When true, excludes the column from both the rendered table and the column editor. The column is still included in CSV exports. Useful for ID columns or metadata that should be exported but not displayed or toggled by users. |
Custom Column Editor Layout
customRenderer replaces the default popout. When the table uses left and right pin regions, the built-in layout may show left-pinned, main, and right-pinned columns as separate lists. Set allowColumnPinning: false on columnEditorConfig to hide pin controls while keeping drag and visibility toggles (ColumnEditorConfig). Besides searchSection, listSection, and resetColumns, you can receive pinnedLeftList, unpinnedList, and pinnedRightList when the UI is split by pin section.
1import { SimpleTable } from "@simple-table/react";2import type { Theme, ReactColumnEditorConfig, ColumnEditorRowRendererProps } from "@simple-table/react";3import { columnEditorCustomRendererConfig } from "./column-editor-custom-renderer.demo-data";4import "@simple-table/react/styles.css";56const CustomRowRenderer = ({ header, components }: ColumnEditorRowRendererProps) => (7 <div8 style={{9 display: "flex",10 alignItems: "center",11 gap: 8,12 padding: "6px 8px",13 borderRadius: 6,14 background: "#f8fafc",15 marginBottom: 4,16 }}17 >18 {components.checkbox != null && <span>{components.checkbox}</span>}19 <span style={{ flex: 1, fontSize: 13, fontWeight: 500 }}>{header.label}</span>20 {components.dragIcon != null && (21 <span style={{ cursor: "grab", opacity: 0.5 }}>{components.dragIcon}</span>22 )}23 </div>24);2526const columnEditorConfig: ReactColumnEditorConfig = {27 text: "Manage Columns",28 searchEnabled: true,29 searchPlaceholder: "Search columns…",30 rowRenderer: CustomRowRenderer,31};3233const ColumnEditorCustomRendererDemo = ({34 height = "400px",35 theme,36}: {37 height?: string | number;38 theme?: Theme;39}) => {40 return (41 <SimpleTable42 columns={columnEditorCustomRendererConfig.headers}43 rows={columnEditorCustomRendererConfig.rows}44 enableColumnEditor45 columnEditorConfig={columnEditorConfig}46 height={height}47 theme={theme}48 />49 );50};5152export default ColumnEditorCustomRendererDemo;
1<script setup lang="ts">2import { SimpleTable } from "@simple-table/vue";3import type { Theme } from "@simple-table/vue";4import {5 columnEditorCustomRendererConfig,6 COLUMN_EDITOR_TEXT,7 COLUMN_EDITOR_SEARCH_PLACEHOLDER,8} from "./column-editor-custom-renderer.demo-data";9import ColumnEditorCustomRow from "./ColumnEditorCustomRow.vue";10import "@simple-table/vue/styles.css";1112const props = withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {13 height: "400px",14});1516const editorConfig = {17 text: COLUMN_EDITOR_TEXT,18 searchEnabled: true,19 searchPlaceholder: COLUMN_EDITOR_SEARCH_PLACEHOLDER,20 rowRenderer: ColumnEditorCustomRow,21};22</script>2324<template>25 <SimpleTable26 :columns="columnEditorCustomRendererConfig.headers"27 :rows="columnEditorCustomRendererConfig.rows"28 :enable-column-editor="true"29 :column-editor-config="editorConfig"30 :height="props.height"31 :theme="props.theme"32 />33</template>343536// ColumnEditorCustomRow.vue37<script lang="ts">38import { defineComponent, h } from "vue";39import type { ColumnEditorRowRendererProps } from "@simple-table/vue";4041export default defineComponent({42 name: "ColumnEditorCustomRow",43 props: {44 header: { type: Object, required: true },45 components: { type: Object, required: true },46 },47 setup(props: ColumnEditorRowRendererProps) {48 return () =>49 h(50 "div",51 {52 style: {53 display: "flex",54 alignItems: "center",55 gap: "8px",56 padding: "6px 8px",57 borderRadius: "6px",58 background: "#f8fafc",59 marginBottom: "4px",60 },61 },62 [63 props.components.checkbox != null64 ? h("span", null, [props.components.checkbox as object])65 : null,66 h(67 "span",68 { style: { flex: "1", fontSize: "13px", fontWeight: "500" } },69 props.header.label,70 ),71 props.components.dragIcon != null72 ? h(73 "span",74 { style: { cursor: "grab", opacity: "0.5" } },75 [props.components.dragIcon as object],76 )77 : null,78 ].filter(Boolean),79 );80 },81});82</script>83
1import { Component, Input } from "@angular/core";2import { SimpleTableComponent } from "@simple-table/angular";3import type { AngularColumnEditorConfig, AngularColumnDef, Row, Theme } from "@simple-table/angular";4import {5 columnEditorCustomRendererConfig,6 COLUMN_EDITOR_SEARCH_PLACEHOLDER,7 COLUMN_EDITOR_TEXT,8} from "./column-editor-custom-renderer.demo-data";9import { ColumnEditorCustomRowComponent } from "./column-editor-custom-row.component";10import "@simple-table/angular/styles.css";1112@Component({13 selector: "column-editor-custom-renderer-demo",14 standalone: true,15 imports: [SimpleTableComponent],16 template: `17 <simple-table18 [rows]="rows"19 [columns]="headers"20 [height]="height"21 [theme]="theme"22 [enableColumnEditor]="true"23 [columnEditorConfig]="editorConfig"24 ></simple-table>25 `,26})27export class ColumnEditorCustomRendererDemoComponent {28 @Input() height: string | number = "400px";29 @Input() theme?: Theme;3031 readonly rows: Row[] = columnEditorCustomRendererConfig.rows;32 readonly headers: AngularColumnDef[] = columnEditorCustomRendererConfig.headers;33 readonly editorConfig: AngularColumnEditorConfig = {34 text: COLUMN_EDITOR_TEXT,35 searchEnabled: true,36 searchPlaceholder: COLUMN_EDITOR_SEARCH_PLACEHOLDER,37 rowRenderer: ColumnEditorCustomRowComponent,38 };39}404142// column-editor-custom-renderer.demo-data.ts43// Self-contained demo table setup for this example.44import type { AngularColumnDef, Row } from "@simple-table/angular";454647export const columnEditorCustomRendererData: Row[] = [48 { id: 1, name: "Alice Johnson", email: "alice@example.com", role: "Engineer", salary: 125000, department: "Engineering", status: "active" },49 { id: 2, name: "Bob Martinez", email: "bob@example.com", role: "Designer", salary: 98000, department: "Design", status: "active" },50 { id: 3, name: "Clara Chen", email: "clara@example.com", role: "PM", salary: 115000, department: "Product", status: "inactive" },51 { id: 4, name: "David Kim", email: "david@example.com", role: "Engineer", salary: 132000, department: "Engineering", status: "active" },52 { id: 5, name: "Elena Rossi", email: "elena@example.com", role: "Analyst", salary: 89000, department: "Analytics", status: "active" },53 { id: 6, name: "Frank Müller", email: "frank@example.com", role: "Engineer", salary: 118000, department: "Engineering", status: "inactive" },54 { id: 7, name: "Grace Park", email: "grace@example.com", role: "Designer", salary: 105000, department: "Design", status: "active" },55 { id: 8, name: "Henry Patel", email: "henry@example.com", role: "Lead", salary: 145000, department: "Engineering", status: "active" },56];5758export const columnEditorCustomRendererHeaders: AngularColumnDef[] = [59 { accessor: "id", label: "ID", width: 60, type: "number" },60 { accessor: "name", label: "Name", width: 170, type: "string", sortable: true },61 { accessor: "email", label: "Email", width: 200, type: "string" },62 { accessor: "role", label: "Role", width: 130, type: "string", sortable: true },63 {64 accessor: "salary",65 label: "Salary",66 width: 130,67 type: "number",68 sortable: true,69 valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`,70 },71 { accessor: "department", label: "Department", width: 140, type: "string", sortable: true },72 { accessor: "status", label: "Status", width: 100, type: "string" },73];7475export const columnEditorCustomRendererConfig = {76 headers: columnEditorCustomRendererHeaders,77 rows: columnEditorCustomRendererData,78 tableProps: {79 enableColumnEditor: true,80 },81} as const;8283export const COLUMN_EDITOR_TEXT = "Manage Columns";84export const COLUMN_EDITOR_SEARCH_PLACEHOLDER = "Search columns…";858687// column-editor-custom-row.component.ts88import {89 AfterViewInit,90 Component,91 ElementRef,92 Input,93 OnChanges,94 ViewChild,95} from "@angular/core";96import type { ColumnEditorRowRendererProps } from "@simple-table/angular";9798function attach(slot: unknown, host: HTMLElement | undefined): void {99 if (!host) return;100 host.replaceChildren();101 if (slot == null) return;102 if (typeof slot === "string") {103 host.textContent = slot;104 } else if (slot instanceof Node) {105 host.appendChild(slot);106 }107}108109@Component({110 standalone: true,111 selector: "demo-column-editor-custom-row",112 template: `113 <div114 style="display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:6px;background:#f8fafc;margin-bottom:4px;"115 >116 @if (components.checkbox) {117 <span #checkboxHost></span>118 }119 <span style="flex:1;font-size:13px;font-weight:500;">{{ header.label }}</span>120 @if (components.dragIcon) {121 <span #dragHost style="cursor:grab;opacity:0.5;"></span>122 }123 </div>124 `,125})126export class ColumnEditorCustomRowComponent implements AfterViewInit, OnChanges {127 @Input({ required: true }) header!: ColumnEditorRowRendererProps["header"];128 @Input({ required: true }) components!: ColumnEditorRowRendererProps["components"];129 @Input() accessor?: ColumnEditorRowRendererProps["accessor"];130 @Input() panelSection?: ColumnEditorRowRendererProps["panelSection"];131 @Input() essential?: ColumnEditorRowRendererProps["essential"];132 @Input() canToggleVisibility?: ColumnEditorRowRendererProps["canToggleVisibility"];133 @Input() allowColumnPinning?: ColumnEditorRowRendererProps["allowColumnPinning"];134 @Input() pinControl?: ColumnEditorRowRendererProps["pinControl"];135136 @ViewChild("checkboxHost") checkboxRef?: ElementRef<HTMLSpanElement>;137 @ViewChild("dragHost") dragRef?: ElementRef<HTMLSpanElement>;138139 ngAfterViewInit(): void {140 this.syncSlots();141 }142143 ngOnChanges(): void {144 this.syncSlots();145 }146147 private syncSlots(): void {148 attach(this.components.checkbox, this.checkboxRef?.nativeElement);149 attach(this.components.dragIcon, this.dragRef?.nativeElement);150 }151}152
1<script lang="ts">2 import { SimpleTable } from "@simple-table/svelte";3 import type { Theme } from "@simple-table/svelte";4 import {5 columnEditorCustomRendererConfig,6 COLUMN_EDITOR_TEXT,7 COLUMN_EDITOR_SEARCH_PLACEHOLDER,8 } from "./column-editor-custom-renderer.demo-data";9 import ColumnEditorCustomRow from "./ColumnEditorCustomRow.svelte";10 import "@simple-table/svelte/styles.css";1112 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();1314 const editorConfig = {15 text: COLUMN_EDITOR_TEXT,16 searchEnabled: true,17 searchPlaceholder: COLUMN_EDITOR_SEARCH_PLACEHOLDER,18 rowRenderer: ColumnEditorCustomRow,19 };20</script>2122<SimpleTable23 columns={columnEditorCustomRendererConfig.headers}24 rows={columnEditorCustomRendererConfig.rows}25 enableColumnEditor={true}26 columnEditorConfig={editorConfig}27 {height}28 {theme}29/>303132// ColumnEditorCustomRow.svelte33<script lang="ts">34 import type { ColumnEditorRowRendererProps } from "@simple-table/svelte";3536 let { header, components }: ColumnEditorRowRendererProps = $props();3738 let checkboxHost: HTMLSpanElement | undefined = $state(undefined);39 let dragHost: HTMLSpanElement | undefined = $state(undefined);4041 function attach(slot: unknown, host: HTMLElement | undefined): void {42 if (!host) return;43 host.replaceChildren();44 if (slot == null) return;45 if (typeof slot === "string") {46 host.textContent = slot;47 } else if (slot instanceof Node) {48 host.appendChild(slot);49 }50 }5152 $effect(() => {53 attach(components.checkbox, checkboxHost);54 });5556 $effect(() => {57 attach(components.dragIcon, dragHost);58 });59</script>6061<div62 style="display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 6px; background: #f8fafc; margin-bottom: 4px;"63>64 {#if components.checkbox}65 <span bind:this={checkboxHost}></span>66 {/if}67 <span style="flex: 1; font-size: 13px; font-weight: 500;">{header.label}</span>68 {#if components.dragIcon}69 <span bind:this={dragHost} style="cursor: grab; opacity: 0.5;"></span>70 {/if}71</div>72
1import {2 SimpleTable,3 type ColumnEditorRowRendererProps,4 type SolidColumnEditorConfig,5 type Theme,6} from "@simple-table/solid";7import { createEffect } from "solid-js";8import {9 columnEditorCustomRendererConfig,10 COLUMN_EDITOR_TEXT,11 COLUMN_EDITOR_SEARCH_PLACEHOLDER,12} from "./column-editor-custom-renderer.demo-data";13import "@simple-table/solid/styles.css";1415function attach(slot: unknown, host: HTMLElement | undefined): void {16 if (!host) return;17 host.replaceChildren();18 if (slot == null) return;19 if (typeof slot === "string") {20 host.textContent = slot;21 } else if (slot instanceof Node) {22 host.appendChild(slot);23 }24}2526function SlotHost(props: { slot: () => unknown; style?: string }) {27 let host: HTMLSpanElement | undefined;28 createEffect(() => {29 attach(props.slot(), host);30 });31 return (32 <span33 ref={(el) => {34 host = el;35 attach(props.slot(), el);36 }}37 style={props.style}38 />39 );40}4142const CustomRowRenderer = (p: ColumnEditorRowRendererProps) => (43 <div44 style={{45 display: "flex",46 "align-items": "center",47 gap: "8px",48 padding: "6px 8px",49 "border-radius": "6px",50 background: "#f8fafc",51 "margin-bottom": "4px",52 }}53 >54 {p.components.checkbox && <SlotHost slot={() => p.components.checkbox} />}55 <span style={{ flex: "1", "font-size": "13px", "font-weight": "500" }}>{p.header.label}</span>56 {p.components.dragIcon && (57 <SlotHost slot={() => p.components.dragIcon} style={{ cursor: "grab", opacity: "0.5" }} />58 )}59 </div>60);6162const columnEditorConfig: SolidColumnEditorConfig = {63 text: COLUMN_EDITOR_TEXT,64 searchEnabled: true,65 searchPlaceholder: COLUMN_EDITOR_SEARCH_PLACEHOLDER,66 rowRenderer: CustomRowRenderer,67};6869export default function ColumnEditorCustomRendererDemo(props: { height?: string | number; theme?: Theme }) {70 return (71 <SimpleTable72 columns={columnEditorCustomRendererConfig.headers}73 rows={columnEditorCustomRendererConfig.rows}74 enableColumnEditor75 columnEditorConfig={columnEditorConfig}76 height={props.height ?? "400px"}77 theme={props.theme}78 />79 );80}
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme } from "simple-table-core";3import {4 columnEditorCustomRendererConfig,5 COLUMN_EDITOR_TEXT,6 COLUMN_EDITOR_SEARCH_PLACEHOLDER,7 buildVanillaColumnEditorRowRenderer,8} from "./column-editor-custom-renderer.demo-data";9import "simple-table-core/styles.css";1011export function renderColumnEditorCustomRendererDemo(12 container: HTMLElement,13 options?: { height?: string | number; theme?: Theme }14): SimpleTableVanilla {15 const table = new SimpleTableVanilla(container, {16 columns: [...columnEditorCustomRendererConfig.headers],17 rows: columnEditorCustomRendererConfig.rows,18 height: options?.height ?? "400px",19 theme: options?.theme,20 enableColumnEditor: true,21 columnEditorConfig: {22 text: COLUMN_EDITOR_TEXT,23 searchEnabled: true,24 searchPlaceholder: COLUMN_EDITOR_SEARCH_PLACEHOLDER,25 rowRenderer: buildVanillaColumnEditorRowRenderer,26 },27 });28 return table;29}303132// column-editor-custom-renderer.demo-data.ts33// Self-contained demo table setup for this example.34import type { ColumnDef, Row, ColumnEditorRowRendererProps } from "simple-table-core";353637export const columnEditorCustomRendererData: Row[] = [38 { id: 1, name: "Alice Johnson", email: "alice@example.com", role: "Engineer", salary: 125000, department: "Engineering", status: "active" },39 { id: 2, name: "Bob Martinez", email: "bob@example.com", role: "Designer", salary: 98000, department: "Design", status: "active" },40 { id: 3, name: "Clara Chen", email: "clara@example.com", role: "PM", salary: 115000, department: "Product", status: "inactive" },41 { id: 4, name: "David Kim", email: "david@example.com", role: "Engineer", salary: 132000, department: "Engineering", status: "active" },42 { id: 5, name: "Elena Rossi", email: "elena@example.com", role: "Analyst", salary: 89000, department: "Analytics", status: "active" },43 { id: 6, name: "Frank Müller", email: "frank@example.com", role: "Engineer", salary: 118000, department: "Engineering", status: "inactive" },44 { id: 7, name: "Grace Park", email: "grace@example.com", role: "Designer", salary: 105000, department: "Design", status: "active" },45 { id: 8, name: "Henry Patel", email: "henry@example.com", role: "Lead", salary: 145000, department: "Engineering", status: "active" },46];4748export const columnEditorCustomRendererHeaders: ColumnDef[] = [49 { accessor: "id", label: "ID", width: 60, type: "number" },50 { accessor: "name", label: "Name", width: 170, type: "string", sortable: true },51 { accessor: "email", label: "Email", width: 200, type: "string" },52 { accessor: "role", label: "Role", width: 130, type: "string", sortable: true },53 {54 accessor: "salary",55 label: "Salary",56 width: 130,57 type: "number",58 sortable: true,59 valueFormatter: ({ value }) => `$${(value as number).toLocaleString()}`,60 },61 { accessor: "department", label: "Department", width: 140, type: "string", sortable: true },62 { accessor: "status", label: "Status", width: 100, type: "string" },63];6465export const columnEditorCustomRendererConfig = {66 headers: columnEditorCustomRendererHeaders,67 rows: columnEditorCustomRendererData,68 tableProps: {69 enableColumnEditor: true,70 },71} as const;7273export const COLUMN_EDITOR_TEXT = "Manage Columns";74export const COLUMN_EDITOR_SEARCH_PLACEHOLDER = "Search columns…";7576export function buildVanillaColumnEditorRowRenderer(props: ColumnEditorRowRendererProps): HTMLElement {77 const row = document.createElement("div");78 Object.assign(row.style, {79 display: "flex",80 alignItems: "center",81 gap: "8px",82 padding: "6px 8px",83 borderRadius: "6px",84 background: "#f8fafc",85 marginBottom: "4px",86 });8788 if (props.components.checkbox) {89 const span = document.createElement("span");90 if (typeof props.components.checkbox === "string") {91 span.innerHTML = props.components.checkbox;92 } else {93 span.appendChild(props.components.checkbox as Node);94 }95 row.appendChild(span);96 }9798 const label = document.createElement("span");99 Object.assign(label.style, { flex: "1", fontSize: "13px", fontWeight: "500" });100 label.textContent = props.header.label;101 row.appendChild(label);102103 if (props.components.dragIcon) {104 const span = document.createElement("span");105 Object.assign(span.style, { cursor: "grab", opacity: "0.5" });106 if (typeof props.components.dragIcon === "string") {107 span.innerHTML = props.components.dragIcon;108 } else {109 span.appendChild(props.components.dragIcon as Node);110 }111 row.appendChild(span);112 }113114 return row;115}116
Custom Column Editor Row Layout
rowRenderer controls each row’s layout; props include panelSection, essential, canToggleVisibility, allowColumnPinning, and pinControl. See ColumnEditorRowRendererProps.