Documentation
Footer Renderer
1import {SimpleTable} from "@simple-table/react";import type { Theme, FooterRendererProps } from "@simple-table/react";2import { footerRendererConfig } from "./footer-renderer.demo-data";3import "@simple-table/react/styles.css";45function getFooterColors(theme?: Theme) {6 const isModernDark = theme === "modern-dark";7 const isDark = theme === "dark" || isModernDark;8 const isModernLight = theme === "modern-light";9 const isLight = theme === "light" || isModernLight;1011 if (isModernDark)12 return {13 background: "#1f2937", border: "#374151", text: "#d1d5db",14 buttonBg: "#374151", buttonBorder: "#4b5563", buttonActive: "#3b82f6",15 buttonText: "#d1d5db", buttonDisabled: "#6b7280",16 };17 if (isDark)18 return {19 background: "#1f2937", border: "#374151", text: "#e5e7eb",20 buttonBg: "#374151", buttonBorder: "#4b5563", buttonActive: "#3b82f6",21 buttonText: "#d1d5db", buttonDisabled: "#6b7280",22 };23 if (isLight)24 return {25 background: "white", border: "#f3f4f6", text: "#6b7280",26 buttonBg: "white", buttonBorder: "#e5e7eb", buttonActive: "#3b82f6",27 buttonText: "#374151", buttonDisabled: "#d1d5db",28 };29 return {30 background: "#f8fafc", border: "#e2e8f0", text: "#475569",31 buttonBg: "white", buttonBorder: "#e2e8f0", buttonActive: "#3b82f6",32 buttonText: "#64748b", buttonDisabled: "#cbd5e1",33 };34}3536const FooterRendererDemo = ({37 height = "400px",38 theme,39}: {40 height?: string | number;41 theme?: Theme;42}) => {43 const c = getFooterColors(theme);4445 return (46 <SimpleTable47 defaultHeaders={footerRendererConfig.headers}48 rows={footerRendererConfig.rows}49 shouldPaginate={true}50 rowsPerPage={10}51 height={height}52 theme={theme}53 footerRenderer={({54 currentPage,55 startRow,56 endRow,57 totalRows,58 totalPages,59 hasPrevPage,60 hasNextPage,61 onPrevPage,62 onNextPage,63 onPageChange,64 }: FooterRendererProps) => (65 <div66 style={{67 display: "flex",68 alignItems: "center",69 justifyContent: "space-between",70 padding: "16px 20px",71 backgroundColor: c.background,72 borderTop: `2px solid ${c.border}`,73 }}74 >75 <div style={{ display: "flex", alignItems: "center", gap: "12px" }}>76 <span style={{ fontSize: "14px", fontWeight: 600, color: c.text }}>77 Showing {startRow}-{endRow} of {totalRows} items78 </span>79 </div>8081 <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>82 <button83 onClick={onPrevPage}84 disabled={!hasPrevPage}85 style={{86 padding: "8px 16px", fontSize: "14px", fontWeight: 500,87 color: hasPrevPage ? c.buttonActive : c.buttonDisabled,88 backgroundColor: c.buttonBg, border: `1px solid ${c.buttonBorder}`,89 borderRadius: "6px", cursor: hasPrevPage ? "pointer" : "not-allowed",90 transition: "all 0.2s",91 }}92 >93 Previous94 </button>9596 <div style={{ display: "flex", gap: "4px" }}>97 {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (98 <button99 key={page}100 onClick={() => onPageChange(page)}101 style={{102 padding: "8px 12px", fontSize: "14px", fontWeight: 500,103 color: currentPage === page ? "white" : c.buttonText,104 backgroundColor: currentPage === page ? c.buttonActive : c.buttonBg,105 border: `1px solid ${c.buttonBorder}`, borderRadius: "6px",106 cursor: "pointer", transition: "all 0.2s", minWidth: "40px",107 }}108 >109 {page}110 </button>111 ))}112 </div>113114 <button115 onClick={onNextPage}116 disabled={!hasNextPage}117 style={{118 padding: "8px 16px", fontSize: "14px", fontWeight: 500,119 color: hasNextPage ? c.buttonActive : c.buttonDisabled,120 backgroundColor: c.buttonBg, border: `1px solid ${c.buttonBorder}`,121 borderRadius: "6px", cursor: hasNextPage ? "pointer" : "not-allowed",122 transition: "all 0.2s",123 }}124 >125 Next126 </button>127 </div>128 </div>129 )}130 />131 );132};133134export default FooterRendererDemo;
1<script setup lang="ts">2import { defineComponent, h } from "vue";3import { SimpleTable } from "@simple-table/vue";4import type { Theme, FooterRendererProps } from "@simple-table/vue";5import { footerRendererConfig } from "./footer-renderer.demo-data";6import "@simple-table/vue/styles.css";78const props = withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {9 height: "400px",10});1112function getFooterColors(theme?: Theme) {13 switch (theme) {14 case "modern-dark":15 case "dark":16 return {17 background: "#1f2937",18 border: "#374151",19 text: "#d1d5db",20 buttonBg: "#374151",21 buttonBorder: "#4b5563",22 buttonActive: "#3b82f6",23 buttonText: "#d1d5db",24 buttonDisabled: "#6b7280",25 };26 case "light":27 case "modern-light":28 return {29 background: "white",30 border: "#f3f4f6",31 text: "#6b7280",32 buttonBg: "white",33 buttonBorder: "#e5e7eb",34 buttonActive: "#3b82f6",35 buttonText: "#374151",36 buttonDisabled: "#d1d5db",37 };38 default:39 return {40 background: "#f8fafc",41 border: "#e2e8f0",42 text: "#475569",43 buttonBg: "white",44 buttonBorder: "#e2e8f0",45 buttonActive: "#3b82f6",46 buttonText: "#64748b",47 buttonDisabled: "#cbd5e1",48 };49 }50}5152const FooterBar = defineComponent({53 name: "FooterBar",54 setup(fp: FooterRendererProps) {55 return () => {56 const c = getFooterColors(props.theme);57 const btnStyle = (disabled: boolean, active = false) => ({58 padding: "8px 16px",59 fontSize: "14px",60 fontWeight: "500",61 color: active ? "white" : disabled ? c.buttonDisabled : c.buttonActive,62 backgroundColor: active ? c.buttonActive : c.buttonBg,63 border: `1px solid ${c.buttonBorder}`,64 borderRadius: "6px",65 cursor: disabled ? ("not-allowed" as const) : ("pointer" as const),66 transition: "all 0.2s",67 minWidth: "40px",68 });69 return h(70 "div",71 {72 style: {73 display: "flex",74 alignItems: "center",75 justifyContent: "space-between",76 padding: "16px 20px",77 backgroundColor: c.background,78 borderTop: `2px solid ${c.border}`,79 },80 },81 [82 h(83 "span",84 { style: { fontSize: "14px", fontWeight: "600", color: c.text } },85 `Showing ${fp.startRow}–${fp.endRow} of ${fp.totalRows} items`,86 ),87 h(88 "div",89 { style: { display: "flex", alignItems: "center", gap: "8px" } },90 [91 h("button", {92 style: btnStyle(!fp.hasPrevPage),93 disabled: !fp.hasPrevPage,94 onClick: fp.onPrevPage,95 }, "Previous"),96 h(97 "div",98 { style: { display: "flex", gap: "4px" } },99 Array.from({ length: fp.totalPages }, (_, i) => i + 1).map((page) =>100 h("button", {101 key: page,102 style: { ...btnStyle(false, page === fp.currentPage), padding: "8px 12px" },103 onClick: () => fp.onPageChange(page),104 }, String(page)),105 ),106 ),107 h("button", {108 style: btnStyle(!fp.hasNextPage),109 disabled: !fp.hasNextPage,110 onClick: () => {111 void fp.onNextPage();112 },113 }, "Next"),114 ],115 ),116 ],117 );118 };119 },120});121</script>122123<template>124 <SimpleTable125 :default-headers="footerRendererConfig.headers"126 :rows="footerRendererConfig.rows"127 :footer-renderer="FooterBar"128 :should-paginate="true"129 :rows-per-page="10"130 :hide-footer="false"131 :height="props.height"132 :theme="props.theme"133 />134</template>
1import { signal } from "@angular/core";2import type { Theme } from "@simple-table/angular";34export const footerDemoThemeContext = signal<Theme | undefined>(undefined);567// footer-pagination.component.ts8import { Component, computed, Input } from "@angular/core";9import type { FooterRendererProps } from "@simple-table/angular";10import type { Theme } from "@simple-table/angular";11import { footerDemoThemeContext } from "./footer-demo-theme-context";1213function palette(theme?: Theme) {14 switch (theme) {15 case "modern-dark":16 case "dark":17 return {18 background: "#1f2937",19 border: "#374151",20 text: "#d1d5db",21 buttonBg: "#374151",22 buttonBorder: "#4b5563",23 buttonActive: "#3b82f6",24 buttonText: "#d1d5db",25 buttonDisabled: "#6b7280",26 };27 case "light":28 case "modern-light":29 return {30 background: "white",31 border: "#f3f4f6",32 text: "#6b7280",33 buttonBg: "white",34 buttonBorder: "#e5e7eb",35 buttonActive: "#3b82f6",36 buttonText: "#374151",37 buttonDisabled: "#d1d5db",38 };39 default:40 return {41 background: "#f8fafc",42 border: "#e2e8f0",43 text: "#475569",44 buttonBg: "white",45 buttonBorder: "#e2e8f0",46 buttonActive: "#3b82f6",47 buttonText: "#64748b",48 buttonDisabled: "#cbd5e1",49 };50 }51}5253@Component({54 standalone: true,55 selector: "demo-footer-pagination",56 template: `57 <div58 style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;"59 [style.background-color]="colors().background"60 [style.border-top]="'2px solid ' + colors().border"61 >62 <span style="font-size:14px;font-weight:600;" [style.color]="colors().text">63 Showing {{ startRow }}–{{ endRow }} of {{ totalRows }} items64 </span>65 <div style="display:flex;align-items:center;gap:8px;">66 <button67 type="button"68 [disabled]="!hasPrevPage"69 (click)="onPrevPage()"70 [style.padding]="'8px 16px'"71 [style.font-size]="'14px'"72 [style.font-weight]="'500'"73 [style.color]="btnStylePrev().color"74 [style.background-color]="btnStylePrev().bg"75 [style.border]="'1px solid ' + colors().buttonBorder"76 [style.border-radius]="'6px'"77 [style.cursor]="hasPrevPage ? 'pointer' : 'not-allowed'"78 [style.min-width]="'40px'"79 >80 Previous81 </button>82 <div style="display:flex;gap:4px;">83 @for (p of pageNumbers(); track p) {84 <button85 type="button"86 (click)="onPageChange(p)"87 [style.padding]="'8px 12px'"88 [style.font-size]="'14px'"89 [style.font-weight]="'500'"90 [style.color]="pageBtnStyle(p).color"91 [style.background-color]="pageBtnStyle(p).bg"92 [style.border]="'1px solid ' + colors().buttonBorder"93 [style.border-radius]="'6px'"94 style="cursor:pointer;min-width:40px;"95 >96 {{ p }}97 </button>98 }99 </div>100 <button101 type="button"102 [disabled]="!hasNextPage"103 (click)="onNext()"104 [style.padding]="'8px 16px'"105 [style.font-size]="'14px'"106 [style.font-weight]="'500'"107 [style.color]="btnStyleNext().color"108 [style.background-color]="btnStyleNext().bg"109 [style.border]="'1px solid ' + colors().buttonBorder"110 [style.border-radius]="'6px'"111 [style.cursor]="hasNextPage ? 'pointer' : 'not-allowed'"112 [style.min-width]="'40px'"113 >114 Next115 </button>116 </div>117 </div>118 `,119})120export class FooterPaginationComponent {121 @Input({ required: true }) currentPage!: FooterRendererProps["currentPage"];122 @Input({ required: true }) endRow!: FooterRendererProps["endRow"];123 @Input({ required: true }) hasNextPage!: FooterRendererProps["hasNextPage"];124 @Input({ required: true }) hasPrevPage!: FooterRendererProps["hasPrevPage"];125 @Input({ required: true }) onNextPage!: FooterRendererProps["onNextPage"];126 @Input({ required: true }) onPageChange!: FooterRendererProps["onPageChange"];127 @Input({ required: true }) onPrevPage!: FooterRendererProps["onPrevPage"];128 @Input({ required: true }) rowsPerPage!: FooterRendererProps["rowsPerPage"];129 @Input({ required: true }) startRow!: FooterRendererProps["startRow"];130 @Input({ required: true }) totalPages!: FooterRendererProps["totalPages"];131 @Input({ required: true }) totalRows!: FooterRendererProps["totalRows"];132133 readonly colors = computed(() => palette(footerDemoThemeContext()));134135 pageNumbers(): number[] {136 return Array.from({ length: this.totalPages }, (_, i) => i + 1);137 }138139 btnStylePrev(): { color: string; bg: string } {140 const c = this.colors();141 return {142 color: !this.hasPrevPage ? c.buttonDisabled : c.buttonActive,143 bg: c.buttonBg,144 };145 }146147 btnStyleNext(): { color: string; bg: string } {148 const c = this.colors();149 return {150 color: !this.hasNextPage ? c.buttonDisabled : c.buttonActive,151 bg: c.buttonBg,152 };153 }154155 pageBtnStyle(p: number): { color: string; bg: string } {156 const c = this.colors();157 const active = p === this.currentPage;158 return {159 color: active ? "white" : c.buttonActive,160 bg: active ? c.buttonActive : c.buttonBg,161 };162 }163164 onNext(): void {165 void this.onNextPage();166 }167}168169170// footer-renderer-demo.component.ts171import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from "@angular/core";172import { SimpleTableComponent } from "@simple-table/angular";173import type { AngularHeaderObject, Row, Theme } from "@simple-table/angular";174import { footerDemoThemeContext } from "./footer-demo-theme-context";175import { FooterPaginationComponent } from "./footer-pagination.component";176import { footerRendererConfig } from "./footer-renderer.demo-data";177import "@simple-table/angular/styles.css";178179@Component({180 selector: "footer-renderer-demo",181 standalone: true,182 imports: [SimpleTableComponent],183 template: `184 <simple-table185 [rows]="rows"186 [defaultHeaders]="headers"187 [footerRenderer]="footerRenderer"188 [shouldPaginate]="true"189 [rowsPerPage]="10"190 [hideFooter]="false"191 [height]="height"192 [theme]="theme"193 ></simple-table>194 `,195})196export class FooterRendererDemoComponent implements OnInit, OnChanges, OnDestroy {197 @Input() height: string | number = "400px";198 @Input() theme?: Theme;199200 readonly rows: Row[] = footerRendererConfig.rows;201 readonly headers: AngularHeaderObject[] = footerRendererConfig.headers;202 readonly footerRenderer = FooterPaginationComponent;203204 ngOnInit(): void {205 footerDemoThemeContext.set(this.theme);206 }207208 ngOnChanges(changes: SimpleChanges): void {209 if (changes["theme"]) {210 footerDemoThemeContext.set(this.theme);211 }212 }213214 ngOnDestroy(): void {215 footerDemoThemeContext.set(undefined);216 }217}218219220// footer-renderer.demo-data.ts221// Self-contained demo table setup for this example.222import type { AngularHeaderObject, Row } from "@simple-table/angular";223224225export const footerRendererHeaders: AngularHeaderObject[] = [226 { accessor: "id", label: "ID", width: 60, type: "number" },227 { accessor: "product", label: "Product Name", width: 220, type: "string" },228 { accessor: "category", label: "Category", width: 150, type: "string" },229 { accessor: "price", label: "Price", width: 100, type: "number" },230 { accessor: "stock", label: "Stock", width: 100, type: "number" },231 { accessor: "status", label: "Status", width: "1fr", type: "string" },232];233234export const footerRendererData: Row[] = [235 { id: 1, product: "MacBook Pro 16-inch M3 Max", category: "Laptops", price: 3499, stock: 28, status: "In Stock" },236 { id: 2, product: "Dell XPS 15 OLED Touchscreen", category: "Laptops", price: 2299, stock: 42, status: "In Stock" },237 { id: 3, product: "ThinkPad X1 Carbon Gen 11", category: "Laptops", price: 1899, stock: 35, status: "In Stock" },238 { id: 4, product: "HP Spectre x360 Convertible", category: "Laptops", price: 1649, stock: 51, status: "In Stock" },239 { id: 5, product: "ASUS ROG Strix Gaming Laptop", category: "Laptops", price: 2199, stock: 19, status: "In Stock" },240 { id: 6, product: "Logitech MX Master 3S Wireless", category: "Accessories", price: 99, stock: 342, status: "In Stock" },241 { id: 7, product: "Apple Magic Mouse Black", category: "Accessories", price: 89, stock: 218, status: "In Stock" },242 { id: 8, product: "Razer DeathAdder V3 Pro", category: "Accessories", price: 149, stock: 167, status: "In Stock" },243 { id: 9, product: "Microsoft Surface Precision Mouse", category: "Accessories", price: 79, stock: 203, status: "In Stock" },244 { id: 10, product: "Corsair K95 RGB Platinum XT", category: "Keyboards", price: 199, stock: 89, status: "In Stock" },245 { id: 11, product: "Keychron Q1 Pro Mechanical", category: "Keyboards", price: 189, stock: 134, status: "In Stock" },246 { id: 12, product: "Ducky One 3 TKL RGB", category: "Keyboards", price: 159, stock: 76, status: "In Stock" },247 { id: 13, product: "Leopold FC900R PD Cherry MX", category: "Keyboards", price: 169, stock: 54, status: "In Stock" },248 { id: 14, product: "LG UltraGear 27-inch 4K 144Hz", category: "Monitors", price: 799, stock: 31, status: "In Stock" },249 { id: 15, product: "Samsung Odyssey G9 Curved", category: "Monitors", price: 1299, stock: 18, status: "In Stock" },250 { id: 16, product: "Dell UltraSharp U2723DE 27in", category: "Monitors", price: 649, stock: 47, status: "In Stock" },251 { id: 17, product: "BenQ PD3220U Designer 32in", category: "Monitors", price: 1099, stock: 23, status: "In Stock" },252 { id: 18, product: "ASUS ProArt Display PA279CRV", category: "Monitors", price: 549, stock: 39, status: "In Stock" },253 { id: 19, product: "Sony WH-1000XM5 Noise Cancelling", category: "Audio", price: 399, stock: 127, status: "In Stock" },254 { id: 20, product: "Bose QuietComfort Ultra", category: "Audio", price: 429, stock: 98, status: "In Stock" },255 { id: 21, product: "Apple AirPods Max Space Gray", category: "Audio", price: 549, stock: 82, status: "In Stock" },256 { id: 22, product: "Sennheiser Momentum 4 Wireless", category: "Audio", price: 349, stock: 104, status: "In Stock" },257 { id: 23, product: "Logitech C920 HD Pro Webcam", category: "Video", price: 79, stock: 245, status: "In Stock" },258 { id: 24, product: "Elgato Facecam Pro 4K60", category: "Video", price: 299, stock: 67, status: "In Stock" },259 { id: 25, product: "Razer Kiyo Pro Ultra 4K", category: "Video", price: 329, stock: 41, status: "In Stock" },260 { id: 26, product: "Herman Miller Aeron Ergonomic", category: "Furniture", price: 1695, stock: 12, status: "Low Stock" },261 { id: 27, product: "Steelcase Leap V2 Office Chair", category: "Furniture", price: 1299, stock: 15, status: "In Stock" },262 { id: 28, product: "Autonomous SmartDesk Pro", category: "Furniture", price: 899, stock: 28, status: "In Stock" },263 { id: 29, product: "Uplift V2 Standing Desk Frame", category: "Furniture", price: 749, stock: 34, status: "In Stock" },264 { id: 30, product: "FlexiSpot E7 Plus Adjustable", category: "Furniture", price: 649, stock: 45, status: "In Stock" },265 { id: 31, product: "Anker PowerCore 20000mAh", category: "Power", price: 49, stock: 412, status: "In Stock" },266 { id: 32, product: "RAVPower 60W USB-C Charger", category: "Power", price: 39, stock: 387, status: "In Stock" },267 { id: 33, product: "Belkin BoostCharge Pro 3-in-1", category: "Power", price: 149, stock: 156, status: "In Stock" },268 { id: 34, product: "Samsung T7 Shield 2TB SSD", category: "Storage", price: 199, stock: 234, status: "In Stock" },269 { id: 35, product: "SanDisk Extreme Pro 1TB Portable", category: "Storage", price: 159, stock: 189, status: "In Stock" },270 { id: 36, product: "WD My Passport 5TB External", category: "Storage", price: 139, stock: 276, status: "In Stock" },271 { id: 37, product: "Crucial X9 Pro 4TB Rugged", category: "Storage", price: 289, stock: 143, status: "In Stock" },272 { id: 38, product: "CalDigit TS4 Thunderbolt 4 Dock", category: "Hubs & Docks", price: 399, stock: 52, status: "In Stock" },273 { id: 39, product: "Anker 577 Thunderbolt Docking", category: "Hubs & Docks", price: 299, stock: 78, status: "In Stock" },274 { id: 40, product: "HyperDrive Gen2 16-Port USB-C", category: "Hubs & Docks", price: 249, stock: 91, status: "In Stock" },275 { id: 41, product: "Blue Yeti X Professional USB", category: "Audio", price: 169, stock: 123, status: "In Stock" },276 { id: 42, product: "Shure MV7 Podcast Microphone", category: "Audio", price: 249, stock: 87, status: "In Stock" },277 { id: 43, product: "Elgato Wave:3 Premium USB", category: "Audio", price: 159, stock: 104, status: "In Stock" },278 { id: 44, product: "Rode NT-USB Mini Studio", category: "Audio", price: 99, stock: 167, status: "In Stock" },279 { id: 45, product: "Audio-Technica AT2020USB+", category: "Audio", price: 149, stock: 145, status: "In Stock" },280];281282export const footerRendererConfig = {283 headers: footerRendererHeaders,284 rows: footerRendererData,285 tableProps: {286 shouldPaginate: true,287 rowsPerPage: 10,288 },289} as const;290
1<script lang="ts">2 import { get } from "svelte/store";3 import type { FooterRendererProps, Theme } from "@simple-table/svelte";4 import { footerDemoTheme } from "./footer-demo-theme";56 let fp: FooterRendererProps = $props();78 function getFooterColors(t?: Theme) {9 switch (t) {10 case "modern-dark":11 case "dark":12 return {13 background: "#1f2937",14 border: "#374151",15 text: "#d1d5db",16 buttonBg: "#374151",17 buttonBorder: "#4b5563",18 buttonActive: "#3b82f6",19 buttonDisabled: "#6b7280",20 };21 case "light":22 case "modern-light":23 return {24 background: "white",25 border: "#f3f4f6",26 text: "#6b7280",27 buttonBg: "white",28 buttonBorder: "#e5e7eb",29 buttonActive: "#3b82f6",30 buttonDisabled: "#d1d5db",31 };32 default:33 return {34 background: "#f8fafc",35 border: "#e2e8f0",36 text: "#475569",37 buttonBg: "white",38 buttonBorder: "#e2e8f0",39 buttonActive: "#3b82f6",40 buttonDisabled: "#cbd5e1",41 };42 }43 }4445 let c = $state(getFooterColors(get(footerDemoTheme)));46 $effect(() => {47 c = getFooterColors($footerDemoTheme);48 });4950 function btnStyle(disabled: boolean, active = false): string {51 const color = active ? "white" : disabled ? c.buttonDisabled : c.buttonActive;52 const bg = active ? c.buttonActive : c.buttonBg;53 return [54 `padding:8px 16px`,55 `font-size:14px`,56 `font-weight:500`,57 `color:${color}`,58 `background-color:${bg}`,59 `border:1px solid ${c.buttonBorder}`,60 `border-radius:6px`,61 `cursor:${disabled ? "not-allowed" : "pointer"}`,62 `transition:all 0.2s`,63 `min-width:40px`,64 ].join(";");65 }66</script>6768<div69 style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;background-color:{c.background};border-top:2px solid {c.border};"70>71 <span style="font-size:14px;font-weight:600;color:{c.text};">72 Showing {fp.startRow}–{fp.endRow} of {fp.totalRows} items73 </span>74 <div style="display:flex;align-items:center;gap:8px;">75 <button type="button" style={btnStyle(!fp.hasPrevPage)} disabled={!fp.hasPrevPage} onclick={() => fp.onPrevPage()}>76 Previous77 </button>78 <div style="display:flex;gap:4px;">79 {#each Array.from({ length: fp.totalPages }, (_, i) => i + 1) as page (page)}80 <button81 type="button"82 style="{btnStyle(false, page === fp.currentPage)};padding:8px 12px;"83 onclick={() => fp.onPageChange(page)}84 >85 {page}86 </button>87 {/each}88 </div>89 <button90 type="button"91 style={btnStyle(!fp.hasNextPage)}92 disabled={!fp.hasNextPage}93 onclick={() => void fp.onNextPage()}94 >95 Next96 </button>97 </div>98</div>99100101// FooterRendererDemo.svelte102<script lang="ts">103 import { SimpleTable } from "@simple-table/svelte";104 import type { Theme } from "@simple-table/svelte";105 import { footerRendererConfig } from "./footer-renderer.demo-data";106 import { footerDemoTheme } from "./footer-demo-theme";107 import FooterDemoBar from "./FooterDemoBar.svelte";108 import "@simple-table/svelte/styles.css";109110 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();111112 $effect.pre(() => {113 footerDemoTheme.set(theme);114 });115</script>116117<SimpleTable118 defaultHeaders={footerRendererConfig.headers}119 rows={footerRendererConfig.rows}120 footerRenderer={FooterDemoBar}121 shouldPaginate={true}122 rowsPerPage={10}123 hideFooter={false}124 {height}125 {theme}126/>127
1import {SimpleTable} from "@simple-table/solid";import type { Theme, FooterRendererProps } from "@simple-table/solid";2import { footerRendererConfig } from "./footer-renderer.demo-data";3import { For } from "solid-js";4import "@simple-table/solid/styles.css";56function getFooterColors(theme?: Theme) {7 const isModernDark = theme === "modern-dark";8 const isDark = theme === "dark" || isModernDark;9 const isModernLight = theme === "modern-light";10 const isLight = theme === "light" || isModernLight;1112 if (isModernDark)13 return {14 background: "#1f2937", border: "#374151", text: "#d1d5db",15 buttonBg: "#374151", buttonBorder: "#4b5563", buttonActive: "#3b82f6",16 buttonText: "#d1d5db", buttonDisabled: "#6b7280",17 };18 if (isDark)19 return {20 background: "#1f2937", border: "#374151", text: "#e5e7eb",21 buttonBg: "#374151", buttonBorder: "#4b5563", buttonActive: "#3b82f6",22 buttonText: "#d1d5db", buttonDisabled: "#6b7280",23 };24 if (isLight)25 return {26 background: "white", border: "#f3f4f6", text: "#6b7280",27 buttonBg: "white", buttonBorder: "#e5e7eb", buttonActive: "#3b82f6",28 buttonText: "#374151", buttonDisabled: "#d1d5db",29 };30 return {31 background: "#f8fafc", border: "#e2e8f0", text: "#475569",32 buttonBg: "white", buttonBorder: "#e2e8f0", buttonActive: "#3b82f6",33 buttonText: "#64748b", buttonDisabled: "#cbd5e1",34 };35}3637export default function FooterRendererDemo(props: { height?: string | number; theme?: Theme }) {38 const c = getFooterColors(props.theme);39 const pages = (totalPages: number) => Array.from({ length: totalPages }, (_, i) => i + 1);4041 return (42 <SimpleTable43 defaultHeaders={footerRendererConfig.headers}44 rows={footerRendererConfig.rows}45 shouldPaginate={true}46 rowsPerPage={10}47 height={props.height ?? "400px"}48 theme={props.theme}49 footerRenderer={(fp: FooterRendererProps) => (50 <div51 style={{52 display: "flex",53 "align-items": "center",54 "justify-content": "space-between",55 padding: "16px 20px",56 "background-color": c.background,57 "border-top": `2px solid ${c.border}`,58 }}59 >60 <div style={{ display: "flex", "align-items": "center", gap: "12px" }}>61 <span style={{ "font-size": "14px", "font-weight": "600", color: c.text }}>62 Showing {fp.startRow}-{fp.endRow} of {fp.totalRows} items63 </span>64 </div>6566 <div style={{ display: "flex", "align-items": "center", gap: "8px" }}>67 <button68 onClick={fp.onPrevPage}69 disabled={!fp.hasPrevPage}70 style={{71 padding: "8px 16px", "font-size": "14px", "font-weight": "500",72 color: fp.hasPrevPage ? c.buttonActive : c.buttonDisabled,73 "background-color": c.buttonBg, border: `1px solid ${c.buttonBorder}`,74 "border-radius": "6px",75 cursor: fp.hasPrevPage ? "pointer" : "not-allowed",76 transition: "all 0.2s",77 }}78 >79 Previous80 </button>8182 <div style={{ display: "flex", gap: "4px" }}>83 <For each={pages(fp.totalPages)}>84 {(page) => (85 <button86 onClick={() => fp.onPageChange(page)}87 style={{88 padding: "8px 12px", "font-size": "14px", "font-weight": "500",89 color: fp.currentPage === page ? "white" : c.buttonText,90 "background-color": fp.currentPage === page ? c.buttonActive : c.buttonBg,91 border: `1px solid ${c.buttonBorder}`, "border-radius": "6px",92 cursor: "pointer", transition: "all 0.2s", "min-width": "40px",93 }}94 >95 {page}96 </button>97 )}98 </For>99 </div>100101 <button102 onClick={() => fp.onNextPage()}103 disabled={!fp.hasNextPage}104 style={{105 padding: "8px 16px", "font-size": "14px", "font-weight": "500",106 color: fp.hasNextPage ? c.buttonActive : c.buttonDisabled,107 "background-color": c.buttonBg, border: `1px solid ${c.buttonBorder}`,108 "border-radius": "6px",109 cursor: fp.hasNextPage ? "pointer" : "not-allowed",110 transition: "all 0.2s",111 }}112 >113 Next114 </button>115 </div>116 </div>117 )}118 />119 );120}
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme, FooterRendererProps } from "simple-table-core";3import { footerRendererConfig } from "./footer-renderer.demo-data";4import "simple-table-core/styles.css";56function getFooterColors(theme?: Theme) {7 switch (theme) {8 case "modern-dark":9 case "dark":10 return {11 background: "#1f2937",12 border: "#374151",13 text: "#d1d5db",14 buttonBg: "#374151",15 buttonBorder: "#4b5563",16 buttonActive: "#3b82f6",17 buttonText: "#d1d5db",18 buttonDisabled: "#6b7280",19 };20 case "light":21 case "modern-light":22 return {23 background: "white",24 border: "#f3f4f6",25 text: "#6b7280",26 buttonBg: "white",27 buttonBorder: "#e5e7eb",28 buttonActive: "#3b82f6",29 buttonText: "#374151",30 buttonDisabled: "#d1d5db",31 };32 default:33 return {34 background: "#f8fafc",35 border: "#e2e8f0",36 text: "#475569",37 buttonBg: "white",38 buttonBorder: "#e2e8f0",39 buttonActive: "#3b82f6",40 buttonText: "#64748b",41 buttonDisabled: "#cbd5e1",42 };43 }44}4546function createFooter(props: FooterRendererProps, theme?: Theme): HTMLElement {47 const c = getFooterColors(theme);4849 const wrapper = document.createElement("div");50 Object.assign(wrapper.style, {51 display: "flex",52 alignItems: "center",53 justifyContent: "space-between",54 padding: "16px 20px",55 backgroundColor: c.background,56 borderTop: `2px solid ${c.border}`,57 });5859 const info = document.createElement("span");60 Object.assign(info.style, { fontSize: "14px", fontWeight: "600", color: c.text });61 info.textContent = `Showing ${props.startRow}–${props.endRow} of ${props.totalRows} items`;62 wrapper.appendChild(info);6364 const controls = document.createElement("div");65 Object.assign(controls.style, { display: "flex", alignItems: "center", gap: "8px" });6667 function makeBtn(label: string, onClick: () => void, disabled: boolean, active = false) {68 const btn = document.createElement("button");69 btn.textContent = label;70 Object.assign(btn.style, {71 padding: "8px 16px",72 fontSize: "14px",73 fontWeight: "500",74 color: active ? "white" : disabled ? c.buttonDisabled : c.buttonActive,75 backgroundColor: active ? c.buttonActive : c.buttonBg,76 border: `1px solid ${c.buttonBorder}`,77 borderRadius: "6px",78 cursor: disabled ? "not-allowed" : "pointer",79 transition: "all 0.2s",80 minWidth: "40px",81 });82 btn.disabled = disabled;83 if (!disabled) btn.addEventListener("click", onClick);84 return btn;85 }8687 controls.appendChild(makeBtn("Previous", props.onPrevPage, !props.hasPrevPage));8889 const pages = document.createElement("div");90 Object.assign(pages.style, { display: "flex", gap: "4px" });91 for (let p = 1; p <= props.totalPages; p++) {92 const isActive = p === props.currentPage;93 const btn = makeBtn(String(p), () => props.onPageChange(p), false, isActive);94 btn.style.padding = "8px 12px";95 pages.appendChild(btn);96 }97 controls.appendChild(pages);9899 controls.appendChild(makeBtn("Next", () => props.onNextPage(), !props.hasNextPage));100101 wrapper.appendChild(controls);102 return wrapper;103}104105export function renderFooterRendererDemo(106 container: HTMLElement,107 options?: { height?: string | number; theme?: Theme }108): SimpleTableVanilla {109 const theme = options?.theme;110 const table = new SimpleTableVanilla(container, {111 defaultHeaders: [...footerRendererConfig.headers],112 rows: footerRendererConfig.rows,113 height: options?.height ?? "400px",114 theme,115 shouldPaginate: true,116 rowsPerPage: 10,117 footerRenderer: (props) => createFooter(props, theme),118 hideFooter: false,119 });120 return table;121}122123124// footer-renderer.demo-data.ts125// Self-contained demo table setup for this example.126import type { HeaderObject, Row } from "simple-table-core";127128129export const footerRendererHeaders: HeaderObject[] = [130 { accessor: "id", label: "ID", width: 60, type: "number" },131 { accessor: "product", label: "Product Name", width: 220, type: "string" },132 { accessor: "category", label: "Category", width: 150, type: "string" },133 { accessor: "price", label: "Price", width: 100, type: "number" },134 { accessor: "stock", label: "Stock", width: 100, type: "number" },135 { accessor: "status", label: "Status", width: "1fr", type: "string" },136];137138export const footerRendererData: Row[] = [139 { id: 1, product: "MacBook Pro 16-inch M3 Max", category: "Laptops", price: 3499, stock: 28, status: "In Stock" },140 { id: 2, product: "Dell XPS 15 OLED Touchscreen", category: "Laptops", price: 2299, stock: 42, status: "In Stock" },141 { id: 3, product: "ThinkPad X1 Carbon Gen 11", category: "Laptops", price: 1899, stock: 35, status: "In Stock" },142 { id: 4, product: "HP Spectre x360 Convertible", category: "Laptops", price: 1649, stock: 51, status: "In Stock" },143 { id: 5, product: "ASUS ROG Strix Gaming Laptop", category: "Laptops", price: 2199, stock: 19, status: "In Stock" },144 { id: 6, product: "Logitech MX Master 3S Wireless", category: "Accessories", price: 99, stock: 342, status: "In Stock" },145 { id: 7, product: "Apple Magic Mouse Black", category: "Accessories", price: 89, stock: 218, status: "In Stock" },146 { id: 8, product: "Razer DeathAdder V3 Pro", category: "Accessories", price: 149, stock: 167, status: "In Stock" },147 { id: 9, product: "Microsoft Surface Precision Mouse", category: "Accessories", price: 79, stock: 203, status: "In Stock" },148 { id: 10, product: "Corsair K95 RGB Platinum XT", category: "Keyboards", price: 199, stock: 89, status: "In Stock" },149 { id: 11, product: "Keychron Q1 Pro Mechanical", category: "Keyboards", price: 189, stock: 134, status: "In Stock" },150 { id: 12, product: "Ducky One 3 TKL RGB", category: "Keyboards", price: 159, stock: 76, status: "In Stock" },151 { id: 13, product: "Leopold FC900R PD Cherry MX", category: "Keyboards", price: 169, stock: 54, status: "In Stock" },152 { id: 14, product: "LG UltraGear 27-inch 4K 144Hz", category: "Monitors", price: 799, stock: 31, status: "In Stock" },153 { id: 15, product: "Samsung Odyssey G9 Curved", category: "Monitors", price: 1299, stock: 18, status: "In Stock" },154 { id: 16, product: "Dell UltraSharp U2723DE 27in", category: "Monitors", price: 649, stock: 47, status: "In Stock" },155 { id: 17, product: "BenQ PD3220U Designer 32in", category: "Monitors", price: 1099, stock: 23, status: "In Stock" },156 { id: 18, product: "ASUS ProArt Display PA279CRV", category: "Monitors", price: 549, stock: 39, status: "In Stock" },157 { id: 19, product: "Sony WH-1000XM5 Noise Cancelling", category: "Audio", price: 399, stock: 127, status: "In Stock" },158 { id: 20, product: "Bose QuietComfort Ultra", category: "Audio", price: 429, stock: 98, status: "In Stock" },159 { id: 21, product: "Apple AirPods Max Space Gray", category: "Audio", price: 549, stock: 82, status: "In Stock" },160 { id: 22, product: "Sennheiser Momentum 4 Wireless", category: "Audio", price: 349, stock: 104, status: "In Stock" },161 { id: 23, product: "Logitech C920 HD Pro Webcam", category: "Video", price: 79, stock: 245, status: "In Stock" },162 { id: 24, product: "Elgato Facecam Pro 4K60", category: "Video", price: 299, stock: 67, status: "In Stock" },163 { id: 25, product: "Razer Kiyo Pro Ultra 4K", category: "Video", price: 329, stock: 41, status: "In Stock" },164 { id: 26, product: "Herman Miller Aeron Ergonomic", category: "Furniture", price: 1695, stock: 12, status: "Low Stock" },165 { id: 27, product: "Steelcase Leap V2 Office Chair", category: "Furniture", price: 1299, stock: 15, status: "In Stock" },166 { id: 28, product: "Autonomous SmartDesk Pro", category: "Furniture", price: 899, stock: 28, status: "In Stock" },167 { id: 29, product: "Uplift V2 Standing Desk Frame", category: "Furniture", price: 749, stock: 34, status: "In Stock" },168 { id: 30, product: "FlexiSpot E7 Plus Adjustable", category: "Furniture", price: 649, stock: 45, status: "In Stock" },169 { id: 31, product: "Anker PowerCore 20000mAh", category: "Power", price: 49, stock: 412, status: "In Stock" },170 { id: 32, product: "RAVPower 60W USB-C Charger", category: "Power", price: 39, stock: 387, status: "In Stock" },171 { id: 33, product: "Belkin BoostCharge Pro 3-in-1", category: "Power", price: 149, stock: 156, status: "In Stock" },172 { id: 34, product: "Samsung T7 Shield 2TB SSD", category: "Storage", price: 199, stock: 234, status: "In Stock" },173 { id: 35, product: "SanDisk Extreme Pro 1TB Portable", category: "Storage", price: 159, stock: 189, status: "In Stock" },174 { id: 36, product: "WD My Passport 5TB External", category: "Storage", price: 139, stock: 276, status: "In Stock" },175 { id: 37, product: "Crucial X9 Pro 4TB Rugged", category: "Storage", price: 289, stock: 143, status: "In Stock" },176 { id: 38, product: "CalDigit TS4 Thunderbolt 4 Dock", category: "Hubs & Docks", price: 399, stock: 52, status: "In Stock" },177 { id: 39, product: "Anker 577 Thunderbolt Docking", category: "Hubs & Docks", price: 299, stock: 78, status: "In Stock" },178 { id: 40, product: "HyperDrive Gen2 16-Port USB-C", category: "Hubs & Docks", price: 249, stock: 91, status: "In Stock" },179 { id: 41, product: "Blue Yeti X Professional USB", category: "Audio", price: 169, stock: 123, status: "In Stock" },180 { id: 42, product: "Shure MV7 Podcast Microphone", category: "Audio", price: 249, stock: 87, status: "In Stock" },181 { id: 43, product: "Elgato Wave:3 Premium USB", category: "Audio", price: 159, stock: 104, status: "In Stock" },182 { id: 44, product: "Rode NT-USB Mini Studio", category: "Audio", price: 99, stock: 167, status: "In Stock" },183 { id: 45, product: "Audio-Technica AT2020USB+", category: "Audio", price: 149, stock: 145, status: "In Stock" },184];185186export const footerRendererConfig = {187 headers: footerRendererHeaders,188 rows: footerRendererData,189 tableProps: {190 shouldPaginate: true,191 rowsPerPage: 10,192 },193} as const;194
Footer renderers allow you to completely customize the appearance and functionality of the table footer. Create custom pagination controls, add summary statistics, or build any footer UI that fits your application's design while maintaining full control over navigation and display logic.
Basic Usage
The footerRenderer prop accepts a function that receives pagination state and navigation handlers, returning a custom ReactNode to display in the footer area. This completely replaces the default footer.
Set footerPosition="top" to render the footer above the table body instead of below it. This applies to both the default footer and a custom footerRenderer, and defaults to "bottom".
Footer Renderer Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
footerRenderer(params: FooterRendererProps) => ReactNode | Optional | Custom function to render the table footer. Receives footer information including pagination state and returns a ReactNode for display. Completely replaces the default footer. |
FooterRendererProps Interface
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
currentPagenumber | Required | The current page number (1-based index). | |
startRownumber | Required | The starting row number for the current page (1-based index). | |
endRownumber | Required | The ending row number for the current page (1-based index). | |
totalRowsnumber | Required | The total number of rows in the table. | |
totalPagesnumber | Required | The total number of pages based on rowsPerPage. | |
rowsPerPagenumber | Required | The number of rows displayed per page. | |
hasPrevPageboolean | Required | Boolean indicating if there is a previous page available. | |
hasNextPageboolean | Required | Boolean indicating if there is a next page available. | |
onPrevPage() => void | Required | Function to navigate to the previous page. | |
onNextPage() => Promise<void> | Required | Async function to navigate to the next page. | |
onPageChange(page: number) => void | Required | Function to navigate to a specific page number. | |
prevIconReactNode | Optional | Optional custom icon for the previous page button. | |
nextIconReactNode | Optional | Optional custom icon for the next page button. |
💡 Pro Tip
Use the hasPrevPage and hasNextPage properties to properly disable navigation buttons when users reach the first or last page. This provides better UX feedback and prevents unnecessary navigation attempts.
Common Use Cases
Footer renderers are ideal for various scenarios:
- Custom pagination styles- Match your application's design system with branded pagination controls
- Summary statistics - Display totals, averages, or other aggregated metrics alongside pagination
- Row info display - Show detailed information about visible rows and total dataset size
- Action buttons - Add export, refresh, or other bulk action buttons in the footer area
- Advanced navigation - Implement jump-to-page inputs, page size selectors, or other custom navigation controls
Important Notes
- The footer renderer completely replaces the default footer when provided
- Page numbers are 1-based (first page is 1, not 0)
- The
onNextPagefunction is async and returns a Promise - Make sure to handle the
hasPrevPageandhasNextPageflags to disable navigation when appropriate