Documentation
Cell Renderer
Render custom cell UI: badges, links, progress bars, and other interactive content, with cellRenderer.
Add a cell renderer
Set cellRenderer on a column. Return text, or framework UI (components / DOM nodes). Prefer valueFormatter for plain text formatting. In Angular, prefer an stCell template on the page (it wins over cellRenderer on that column).
React TSX
Copy
const StatusCell = ({ value }) => {const status = String(value);const color = status === "active" ? "#10B981" : "#6B7280";return <span style={{ color, fontWeight: 600 }}>{status}</span>;};const columns: ReactColumnDef[] = [{ accessor: "status", label: "Status", width: 120, cellRenderer: StatusCell },];
Angular
Copy
<simple-table [columns]="columns" [rows]="rows"><ng-template stCell="status" let-value="value"><span[style.color]="value === 'active' ? '#10B981' : '#6B7280'"style="font-weight:600">{{ value }}</span></ng-template></simple-table>// Or pass an Angular component class on the column:// { accessor: "status", cellRenderer: StatusCellComponent }
Vue SFC
Copy
import { h } from "vue";const StatusCell = ({ value }) => {const status = String(value);const color = status === "active" ? "#10B981" : "#6B7280";return h("span", { style: { color, fontWeight: "600" } }, status);};const columns: VueColumnDef[] = [{ accessor: "status", label: "Status", width: 120, cellRenderer: StatusCell },];
Svelte
Copy
<!-- StatusCell.svelte --><script lang="ts">import type { CellRendererProps } from "@simple-table/svelte";let { value }: CellRendererProps = $props();const status = $derived(String(value));const color = $derived(status === "active" ? "#10B981" : "#6B7280");</script><span style="color:{color};font-weight:600">{status}</span><!-- column def -->{ accessor: "status", label: "Status", width: 120, cellRenderer: StatusCell }
Solid TSX
Copy
const StatusCell = (props) => {const status = String(props.value);const color = status === "active" ? "#10B981" : "#6B7280";return <span style={{ color, "font-weight": "600" }}>{status}</span>;};const columns: SolidColumnDef[] = [{ accessor: "status", label: "Status", width: 120, cellRenderer: StatusCell },];
TypeScript
Copy
const StatusCell = ({ value }) => {const status = String(value);const color = status === "active" ? "#10B981" : "#6B7280";const span = document.createElement("span");span.style.color = color;span.style.fontWeight = "600";span.textContent = status;return span;};const columns: ColumnDef[] = [{ accessor: "status", label: "Status", width: 120, cellRenderer: StatusCell },];
Wrap formatted values
Pair with valueFormatter, then read formattedValue in the renderer when you only need custom chrome around already-formatted text.
TypeScript
Copy
{accessor: "salary",label: "Salary",type: "number",valueFormatter: ({ value }) =>typeof value === "number" ? `$${value.toLocaleString()}` : "",cellRenderer: ({ formattedValue }) => (<strong>{formattedValue}</strong>),}
TypeScriptFormat in the column, then wrap the result in the page template.
Copy
{accessor: "salary",label: "Salary",type: "number",valueFormatter: ({ value }) =>typeof value === "number" ? `$${value.toLocaleString()}` : "",}<simple-table [columns]="columns" [rows]="rows"><ng-template stCell="salary" let-formattedValue="formattedValue"><strong>{{ formattedValue }}</strong></ng-template></simple-table>
TypeScript
Copy
{accessor: "salary",label: "Salary",type: "number",valueFormatter: ({ value }) =>typeof value === "number" ? `$${value.toLocaleString()}` : "",cellRenderer: ({ formattedValue }) => h("strong", String(formattedValue ?? "")),}
TypeScript
Copy
{accessor: "salary",label: "Salary",type: "number",valueFormatter: ({ value }) =>typeof value === "number" ? `$${value.toLocaleString()}` : "",cellRenderer: SalaryCell, // receives formattedValue as a prop}
TypeScript
Copy
{accessor: "salary",label: "Salary",type: "number",valueFormatter: (props) =>typeof props.value === "number" ? `$${props.value.toLocaleString()}` : "",cellRenderer: (props) => <strong>{props.formattedValue}</strong>,}
TypeScript
Copy
{accessor: "salary",label: "Salary",type: "number",valueFormatter: ({ value }) =>typeof value === "number" ? `$${value.toLocaleString()}` : "",cellRenderer: ({ formattedValue }) => {const el = document.createElement("strong");el.textContent = String(formattedValue ?? "");return el;},}
Example
Badges, links, progress, and more. Use Code or StackBlitz for the full example.
React TSX
Copy
1import { useMemo } from "react";2import { SimpleTable } from "@simple-table/react";3import type { Theme, CellRendererProps, ReactCellRenderer, ReactColumnDef } from "@simple-table/react";4import { cellRendererConfig } from "./cell-renderer.demo-data";5import type { CellRendererEmployee } from "./cell-renderer.demo-data";6import "@simple-table/react/styles.css";78const getInitials = (name: string) =>9 name10 .split(" ")11 .map((n) => n[0])12 .join("")13 .toUpperCase();1415const TeamCell = ({ row }: CellRendererProps<CellRendererEmployee>) => {16 const members = row.teamMembers;17 return (18 <div style={{ display: "flex", alignItems: "center", gap: 6 }}>19 {members.map((m) => (20 <div key={m.name} style={{ display: "flex", alignItems: "center", gap: 4 }}>21 <div22 style={{23 width: 24,24 height: 24,25 borderRadius: "50%",26 background: "#DBEAFE",27 color: "#1E40AF",28 display: "flex",29 alignItems: "center",30 justifyContent: "center",31 fontSize: 10,32 fontWeight: 600,33 flexShrink: 034 }}35 >36 {getInitials(m.name)}37 </div>38 <span style={{ fontSize: 13, whiteSpace: "nowrap" }}>{m.name}</span>39 </div>40 ))}41 </div>42 );43};4445const WebsiteCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {46 const url = String(value);47 return (48 <span>49 🌐{" "}50 <a51 href={`https://${url}`}52 target="_blank"53 rel="noopener noreferrer"54 style={{ color: "#2563EB", textDecoration: "none" }}55 onMouseEnter={(e) => (e.currentTarget.style.textDecoration = "underline")}56 onMouseLeave={(e) => (e.currentTarget.style.textDecoration = "none")}57 >58 {url}59 </a>60 </span>61 );62};6364const StatusCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {65 const status = String(value);66 const map: Record<string, { icon: string; color: string }> = {67 active: { icon: "✓", color: "#10B981" },68 inactive: { icon: "✕", color: "#EF4444" },69 pending: { icon: "!", color: "#F59E0B" }70 };71 const { icon, color } = map[status] ?? { icon: "?", color: "#6b7280" };72 return (73 <span style={{ color, fontWeight: 600, textTransform: "capitalize" }}>74 {icon} {status}75 </span>76 );77};7879const ProgressCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {80 const pct = Number(value) || 0;81 const color = pct < 30 ? "#EF4444" : pct < 70 ? "#F59E0B" : "#10B981";82 return (83 <div>84 <div style={{ fontSize: 12, marginBottom: 2 }}>{pct}%</div>85 <div style={{ height: 10, background: "#E5E7EB", borderRadius: 5, overflow: "hidden" }}>86 <div87 style={{88 width: `${pct}%`,89 height: "100%",90 background: color,91 borderRadius: 5,92 transition: "width 0.3s"93 }}94 />95 </div>96 </div>97 );98};99100const RatingCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {101 const rating = Number(value) || 0;102 const full = Math.floor(rating);103 const hasHalf = rating % 1 >= 0.25;104 const empty = 5 - full - (hasHalf ? 1 : 0);105 return (106 <span style={{ display: "flex", alignItems: "center", gap: 4 }}>107 <span style={{ color: "#F59E0B", letterSpacing: 1 }}>108 {"★".repeat(full)}109 {hasHalf && <span style={{ opacity: 0.5 }}>★</span>}110 {"☆".repeat(Math.max(0, empty))}111 </span>112 <span style={{ fontSize: 12, color: "#6b7280" }}>{rating}</span>113 </span>114 );115};116117const VerifiedCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {118 const yes = Boolean(value);119 return (120 <span style={{ color: yes ? "#10B981" : "#EF4444", fontWeight: 600 }}>121 {yes ? "✓ Yes" : "✕ No"}122 </span>123 );124};125126const TagsCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {127 const raw = Array.isArray(value) ? value : [];128 const tags = raw.filter((t): t is string => typeof t === "string");129 return (130 <div style={{ display: "flex", gap: 4, flexWrap: "nowrap", overflow: "hidden" }}>131 {tags.map((tag) => (132 <span133 key={tag}134 style={{135 display: "inline-block",136 padding: "2px 8px",137 borderRadius: 4,138 fontSize: 12,139 fontWeight: 500,140 background: "#DBEAFE",141 color: "#1E40AF",142 whiteSpace: "nowrap"143 }}144 >145 {tag}146 </span>147 ))}148 </div>149 );150};151152const CellRendererDemo = ({153 height = "400px",154 theme155}: {156 height?: string | number;157 theme?: Theme;158}) => {159 const headers = useMemo((): ReactColumnDef<CellRendererEmployee>[] => {160 const renderers: Partial<Record<string, ReactCellRenderer<CellRendererEmployee>>> = {161 teamMembers: TeamCell,162 website: WebsiteCell,163 status: StatusCell,164 progress: ProgressCell,165 rating: RatingCell,166 verified: VerifiedCell,167 tags: TagsCell168 };169 return cellRendererConfig.headers.map((h) => {170 const cellRenderer = renderers[String(h.accessor)];171 return cellRenderer ? { ...h, cellRenderer } : h;172 });173 }, []);174175 return (176 <SimpleTable177 columns={headers}178 getRowId={({ row }) => row.id}179 rows={cellRendererConfig.rows}180 height={height}181 theme={theme}182 selectableCells={cellRendererConfig.tableProps.selectableCells}183 customTheme={cellRendererConfig.tableProps.customTheme}184 />185 );186};187188export default CellRendererDemo;
Angularcell-renderer-demo.component.ts
Copy
1import { Component, Input } from "@angular/core";2import { SimpleTableImports } from "@simple-table/angular";3import type { AngularCellRenderer, AngularColumnDef, GetRowIdParams, Theme } from "@simple-table/angular";4import { cellRendererConfig } from "./cell-renderer.demo-data";5import { CrProgressCellComponent } from "./cr-progress-cell.component";6import { CrRatingCellComponent } from "./cr-rating-cell.component";7import { CrTagsCellComponent } from "./cr-tags-cell.component";8import { CrTeamMembersCellComponent } from "./cr-team-members-cell.component";9import { CrVerifiedCellComponent } from "./cr-verified-cell.component";10import { CrWebsiteCellComponent } from "./cr-website-cell.component";11import "@simple-table/angular/styles.css";12import type { CellRendererEmployee } from "./cell-renderer.demo-data";1314const RENDERERS: Partial<Record<string, AngularCellRenderer<CellRendererEmployee>>> = {15 teamMembers: CrTeamMembersCellComponent,16 website: CrWebsiteCellComponent,17 progress: CrProgressCellComponent,18 rating: CrRatingCellComponent,19 verified: CrVerifiedCellComponent,20 tags: CrTagsCellComponent,21};2223const STATUS_META: Record<string, { icon: string; color: string }> = {24 active: { icon: "✓", color: "#10B981" },25 inactive: { icon: "✕", color: "#EF4444" },26 pending: { icon: "!", color: "#F59E0B" },27};2829@Component({30 selector: "cell-renderer-demo",31 standalone: true,32 imports: [SimpleTableImports],33 template: `34 <simple-table35 [getRowId]="getRowId"36 [rows]="rows"37 [columns]="headers"38 [height]="height"39 [theme]="theme"40 [selectableCells]="true"41 [customTheme]="{ rowHeight: 48 }"42 >43 <ng-template stCell="status" let-value="value">44 <span45 [style.color]="statusMeta(value).color"46 style="font-weight:600;text-transform:capitalize;"47 >{{ statusMeta(value).icon }} {{ value }}</span>48 </ng-template>49 </simple-table>50 `,51})52export class CellRendererDemoComponent {53 @Input() height: string | number = "400px";54 @Input() theme?: Theme;5556 readonly rows: CellRendererEmployee[] = cellRendererConfig.rows;57 readonly headers: AngularColumnDef<CellRendererEmployee>[] = cellRendererConfig.headers.map((h): AngularColumnDef<CellRendererEmployee> => {58 const cellRenderer = RENDERERS[String(h.accessor)];59 return cellRenderer ? { ...h, cellRenderer } : { ...h };60 });6162 getRowId = ({ row }: GetRowIdParams<CellRendererEmployee>) => row.id;6364 statusMeta(value: unknown): { icon: string; color: string } {65 return STATUS_META[String(value)] ?? { icon: "?", color: "#6b7280" };66 }67}686970// cell-renderer.demo-data.ts71// Self-contained demo table setup for this example.72import type { AngularColumnDef } from "@simple-table/angular";737475export type CellRendererEmployee = {76 id: number;77 name: string;78 website: string;79 status: string;80 progress: number;81 rating: number;82 verified: boolean;83 tags: string[];84 teamMembers: { name: string; role: string }[];85};8687export const cellRendererData: CellRendererEmployee[] = [88 { id: 1, name: "Isabella Romano", website: "isabellaromano.design", status: "active", progress: 92, rating: 4.9, verified: true, tags: ["UI/UX", "Design", "Frontend"], teamMembers: [{ name: "Alice Smith", role: "Designer" }, { name: "Bob Johnson", role: "Developer" }] },89 { id: 2, name: "Ethan McKenzie", website: "ethanmckenzie.dev", status: "active", progress: 87, rating: 4.7, verified: true, tags: ["Web Development", "Backend", "API"], teamMembers: [{ name: "Charlie Brown", role: "Backend Developer" }, { name: "Diana Prince", role: "Frontend Developer" }] },90 { id: 3, name: "Zoe Patterson", website: "zoepatterson.com", status: "pending", progress: 34, rating: 4.2, verified: false, tags: ["Branding", "Marketing"], teamMembers: [{ name: "Eve Adams", role: "Marketing Manager" }] },91 { id: 4, name: "Felix Chang", website: "felixchang.mobile", status: "active", progress: 95, rating: 4.8, verified: true, tags: ["Mobile App", "UX/UI"], teamMembers: [{ name: "Grace Lee", role: "UX Designer" }, { name: "Hank Johnson", role: "Mobile Developer" }] },92 { id: 5, name: "Aria Gonzalez", website: "ariagonzalez.writer", status: "active", progress: 78, rating: 4.6, verified: true, tags: ["Content Writing", "Copywriting"], teamMembers: [{ name: "Ivy White", role: "Content Strategist" }] },93 { id: 6, name: "Jasper Flynn", website: "jasperflynn.tech", status: "inactive", progress: 12, rating: 3.8, verified: false, tags: ["Consulting", "Tech Strategy"], teamMembers: [{ name: "Kate Brown", role: "Consultant" }] },94 { id: 7, name: "Nova Sterling", website: "novasterling.marketing", status: "active", progress: 83, rating: 4.5, verified: true, tags: ["Digital Marketing", "SEO"], teamMembers: [{ name: "Leo Wilson", role: "SEO Specialist" }, { name: "Mia Davis", role: "Marketing Analyst" }] },95 { id: 8, name: "Cruz Martinez", website: "cruzmartinez.photo", status: "active", progress: 71, rating: 4.4, verified: true, tags: ["Photography", "Videography"], teamMembers: [{ name: "Nina Smith", role: "Photographer" }, { name: "Owen Johnson", role: "Videographer" }] },96 { id: 9, name: "Sage Thompson", website: "sagethompson.ux", status: "active", progress: 89, rating: 4.7, verified: true, tags: ["UX Design", "UI Design"], teamMembers: [{ name: "Pete White", role: "UX Lead" }, { name: "Quinn Brown", role: "UI Designer" }] },97 { id: 10, name: "River Davis", website: "riverdavis.content", status: "pending", progress: 45, rating: 4.1, verified: false, tags: ["Content Strategy", "Copywriting"], teamMembers: [{ name: "Riley Adams", role: "Content Writer" }] },98 { id: 11, name: "Phoenix Williams", website: "phoenixwilliams.digital", status: "active", progress: 93, rating: 4.8, verified: true, tags: ["Digital Consulting", "Strategy"], teamMembers: [{ name: "Sofia Lee", role: "Consultant" }, { name: "Tucker Brown", role: "Digital Strategist" }] },99 { id: 12, name: "Atlas Johnson", website: "atlasjohnson.brand", status: "inactive", progress: 28, rating: 3.6, verified: false, tags: ["Brand Design", "Graphic Design"], teamMembers: [{ name: "Uma Patel", role: "Graphic Designer" }] },100];101102export const cellRendererHeaders: AngularColumnDef<CellRendererEmployee>[] = [103 { accessor: "id", label: "ID", width: 60, type: "number" },104 { accessor: "name", label: "Name", width: 180, type: "string" },105 { accessor: "teamMembers", label: "Team", width: 280, type: "string" },106 { accessor: "website", label: "Website", width: 180, type: "string" },107 { accessor: "status", label: "Status", width: 120, type: "string" },108 { accessor: "progress", label: "Progress", width: 150, type: "number" },109 { accessor: "rating", label: "Rating", width: 150, type: "number" },110 { accessor: "verified", label: "Verified", width: 100, type: "boolean" },111 { accessor: "tags", label: "Tags", width: 250, type: "string" },112];113114export const cellRendererConfig = {115 headers: cellRendererHeaders,116 rows: cellRendererData,117 tableProps: {118 selectableCells: true,119 customTheme: { rowHeight: 48 },120 },121};122123124// cr-progress-cell.component.ts125import { Component, Input } from "@angular/core";126import type { CellValue } from "@simple-table/angular";127128@Component({129 standalone: true,130 selector: "demo-cr-progress",131 template: `132 <div>133 <div style="font-size:12px;margin-bottom:2px;">{{ pct }}%</div>134 <div style="height:10px;background:#E5E7EB;border-radius:5px;overflow:hidden;">135 <div [style.width.%]="pct" [style.background]="barColor" style="height:100%;border-radius:5px;transition:width 0.3s;"></div>136 </div>137 </div>138 `,139})140export class CrProgressCellComponent {141 @Input() value!: CellValue;142143 get pct(): number {144 return Number(this.value) || 0;145 }146147 get barColor(): string {148 const n = this.pct;149 if (n < 30) return "#EF4444";150 if (n < 70) return "#F59E0B";151 return "#10B981";152 }153}154155156// cr-rating-cell.component.ts157import { Component, Input } from "@angular/core";158import type { CellValue } from "@simple-table/angular";159160@Component({161 standalone: true,162 selector: "demo-cr-rating",163 template: `164 <span style="display:flex;align-items:center;gap:4px;">165 <span style="color:#F59E0B;letter-spacing:1px;">166 @for (_ of repeat(fullCount); track $index) {167 ★168 }169 @if (hasHalf) {170 <span style="opacity:0.5;">★</span>171 }172 @for (_ of repeat(emptyCount); track $index) {173 ☆174 }175 </span>176 <span style="font-size:12px;color:#6b7280;">{{ rating }}</span>177 </span>178 `,179})180export class CrRatingCellComponent {181 @Input() value!: CellValue;182183 get rating(): number {184 return Number(this.value) || 0;185 }186187 get fullCount(): number {188 return Math.floor(this.rating);189 }190191 get hasHalf(): boolean {192 return this.rating % 1 >= 0.25;193 }194195 get emptyCount(): number {196 return Math.max(0, 5 - this.fullCount - (this.hasHalf ? 1 : 0));197 }198199 repeat(n: number): number[] {200 return Array.from({ length: n }, (_, i) => i);201 }202}203204205// cr-status-cell.component.ts206import { Component, Input } from "@angular/core";207import type { CellValue } from "@simple-table/angular";208209@Component({210 standalone: true,211 selector: "demo-cr-status",212 template: `213 <span [style.color]="meta.color" style="font-weight:600;text-transform:capitalize;">{{ meta.icon }} {{ status }}</span>214 `,215})216export class CrStatusCellComponent {217 @Input() value!: CellValue;218219 get status(): string {220 return String(this.value);221 }222223 get meta(): { icon: string; color: string } {224 const map: Record<string, { icon: string; color: string }> = {225 active: { icon: "✓", color: "#10B981" },226 inactive: { icon: "✕", color: "#EF4444" },227 pending: { icon: "!", color: "#F59E0B" },228 };229 return map[this.status] ?? { icon: "?", color: "#6b7280" };230 }231}232233234// cr-tags-cell.component.ts235import { Component, Input } from "@angular/core";236237@Component({238 standalone: true,239 selector: "demo-cr-tags",240 template: `241 <div style="display:flex;gap:4px;flex-wrap:nowrap;overflow:hidden;">242 @for (tag of tags; track tag) {243 <span244 style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500;background:#DBEAFE;color:#1E40AF;white-space:nowrap;"245 >{{ tag }}</span>246 }247 </div>248 `,249})250export class CrTagsCellComponent {251 @Input({ required: true }) value!: string[];252253 get tags(): string[] {254 return Array.isArray(this.value) ? this.value : [];255 }256}257258259// cr-team-members-cell.component.ts260import { Component, Input } from "@angular/core";261import type { CellRendererEmployee } from "./cell-renderer.demo-data";262263@Component({264 standalone: true,265 selector: "demo-cr-team-members",266 template: `267 <div style="display:flex;align-items:center;gap:6px;">268 @for (m of members; track m.name) {269 <div style="display:flex;align-items:center;gap:4px;">270 <div271 style="width:24px;height:24px;border-radius:50%;background:#DBEAFE;color:#1E40AF;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0;"272 >273 {{ initials(m.name) }}274 </div>275 <span style="font-size:13px;white-space:nowrap;">{{ m.name }}</span>276 </div>277 }278 </div>279 `,280})281export class CrTeamMembersCellComponent {282 @Input({ required: true }) row!: CellRendererEmployee;283284 get members(): { name: string; role: string }[] {285 return this.row.teamMembers;286 }287288 initials(name: string): string {289 return name.split(" ").map((n) => n[0]).join("").toUpperCase();290 }291}292293294// cr-verified-cell.component.ts295import { Component, Input } from "@angular/core";296import type { CellValue } from "@simple-table/angular";297298@Component({299 standalone: true,300 selector: "demo-cr-verified",301 template: `302 <span [style.color]="yes ? '#10B981' : '#EF4444'" style="font-weight:600;">{{ yes ? "✓ Yes" : "✕ No" }}</span>303 `,304})305export class CrVerifiedCellComponent {306 @Input() value!: CellValue;307308 get yes(): boolean {309 return Boolean(this.value);310 }311}312313314// cr-website-cell.component.ts315import { Component, Input } from "@angular/core";316import type { CellValue } from "@simple-table/angular";317318@Component({319 standalone: true,320 selector: "demo-cr-website",321 template: `322 <span>🌐 <a class="crw" [href]="'https://' + url" target="_blank" rel="noopener noreferrer">{{ url }}</a></span>323 `,324 styles: [325 `326 .crw {327 color: #2563eb;328 text-decoration: none;329 }330 .crw:hover {331 text-decoration: underline;332 }333 `,334 ],335})336export class CrWebsiteCellComponent {337 @Input() value!: CellValue;338339 get url(): string {340 return String(this.value);341 }342}343
Vue SFC
Copy
1<script setup lang="ts">2import { h, computed } from "vue";3import { SimpleTable } from "@simple-table/vue";4import type {5 Theme,6 VueColumnDef,7 VueCellRenderer,8 CellRendererProps,9 GetRowIdParams,10} from "@simple-table/vue";11import { cellRendererConfig } from "./cell-renderer.demo-data";12import type { CellRendererEmployee } from "./cell-renderer.demo-data";13import "@simple-table/vue/styles.css";1415withDefaults(defineProps<{ height?: string | number; theme?: Theme }>(), {16 height: "400px",17});1819const getInitials = (name: string) =>20 name21 .split(" ")22 .map((n) => n[0])23 .join("")24 .toUpperCase();2526const TeamCell = ({ row }: CellRendererProps<CellRendererEmployee>) => {27 const members = row.teamMembers;28 return h(29 "div",30 { style: { display: "flex", alignItems: "center", gap: "6px" } },31 members.map((m) =>32 h("div", { key: m.name, style: { display: "flex", alignItems: "center", gap: "4px" } }, [33 h(34 "div",35 {36 style: {37 width: "24px",38 height: "24px",39 borderRadius: "50%",40 background: "#DBEAFE",41 color: "#1E40AF",42 display: "flex",43 alignItems: "center",44 justifyContent: "center",45 fontSize: "10px",46 fontWeight: "600",47 flexShrink: "0",48 },49 },50 getInitials(m.name),51 ),52 h("span", { style: { fontSize: "13px", whiteSpace: "nowrap" } }, m.name),53 ]),54 ),55 );56};5758const WebsiteCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {59 const url = String(value);60 return h("span", [61 "🌐 ",62 h(63 "a",64 {65 href: `https://${url}`,66 target: "_blank",67 rel: "noopener noreferrer",68 style: { color: "#2563EB", textDecoration: "none" },69 onMouseenter: (e: MouseEvent) => {70 (e.currentTarget as HTMLAnchorElement).style.textDecoration = "underline";71 },72 onMouseleave: (e: MouseEvent) => {73 (e.currentTarget as HTMLAnchorElement).style.textDecoration = "none";74 },75 },76 url,77 ),78 ]);79};8081const StatusCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {82 const status = String(value);83 const map: Record<string, { icon: string; color: string }> = {84 active: { icon: "✓", color: "#10B981" },85 inactive: { icon: "✕", color: "#EF4444" },86 pending: { icon: "!", color: "#F59E0B" },87 };88 const { icon, color } = map[status] ?? { icon: "?", color: "#6b7280" };89 return h(90 "span",91 { style: { color, fontWeight: "600", textTransform: "capitalize" } },92 `${icon} ${status}`,93 );94};9596const ProgressCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {97 const pct = Number(value) || 0;98 const color = pct < 30 ? "#EF4444" : pct < 70 ? "#F59E0B" : "#10B981";99 return h("div", [100 h("div", { style: { fontSize: "12px", marginBottom: "2px" } }, `${pct}%`),101 h(102 "div",103 {104 style: {105 height: "10px",106 background: "#E5E7EB",107 borderRadius: "5px",108 overflow: "hidden",109 },110 },111 [112 h("div", {113 style: {114 width: `${pct}%`,115 height: "100%",116 background: color,117 borderRadius: "5px",118 transition: "width 0.3s",119 },120 }),121 ],122 ),123 ]);124};125126const RatingCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {127 const rating = Number(value) || 0;128 const full = Math.floor(rating);129 const hasHalf = rating % 1 >= 0.25;130 const empty = 5 - full - (hasHalf ? 1 : 0);131 return h("span", { style: { display: "flex", alignItems: "center", gap: "4px" } }, [132 h(133 "span",134 { style: { color: "#F59E0B", letterSpacing: "1px" } },135 [136 "★".repeat(full),137 hasHalf ? h("span", { style: { opacity: 0.5 } }, "★") : null,138 "☆".repeat(Math.max(0, empty)),139 ],140 ),141 h("span", { style: { fontSize: "12px", color: "#6b7280" } }, String(rating)),142 ]);143};144145const VerifiedCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {146 const yes = Boolean(value);147 return h(148 "span",149 { style: { color: yes ? "#10B981" : "#EF4444", fontWeight: "600" } },150 yes ? "✓ Yes" : "✕ No",151 );152};153154const TagsCell = ({ value }: CellRendererProps<CellRendererEmployee>) => {155 const raw = Array.isArray(value) ? value : [];156 const tags = raw.filter((t): t is string => typeof t === "string");157 return h(158 "div",159 { style: { display: "flex", gap: "4px", flexWrap: "nowrap", overflow: "hidden" } },160 tags.map((tag) =>161 h(162 "span",163 {164 key: tag,165 style: {166 display: "inline-block",167 padding: "2px 8px",168 borderRadius: "4px",169 fontSize: "12px",170 fontWeight: "500",171 background: "#DBEAFE",172 color: "#1E40AF",173 whiteSpace: "nowrap",174 },175 },176 tag,177 ),178 ),179 );180};181182const RENDERERS: Partial<Record<string, VueCellRenderer<CellRendererEmployee>>> = {183 teamMembers: TeamCell,184 website: WebsiteCell,185 status: StatusCell,186 progress: ProgressCell,187 rating: RatingCell,188 verified: VerifiedCell,189 tags: TagsCell,190};191192const headers = computed((): VueColumnDef<CellRendererEmployee>[] =>193 cellRendererConfig.headers.map((col) => {194 const cellRenderer = RENDERERS[String(col.accessor)];195 return cellRenderer ? { ...col, cellRenderer } : col;196 }),197);198199const getRowId = ({ row }: GetRowIdParams<CellRendererEmployee>) => row.id;200</script>201202<template>203 <SimpleTable204 :columns="headers"205 :get-row-id="getRowId"206 :rows="cellRendererConfig.rows"207 :height="height"208 :theme="theme"209 :selectable-cells="cellRendererConfig.tableProps.selectableCells"210 :custom-theme="cellRendererConfig.tableProps.customTheme"211 />212</template>
SvelteCellRendererDemo.svelte
Copy
1<script lang="ts">2 import { SimpleTable } from "@simple-table/svelte";3 import type { Theme, SvelteColumnDef, GetRowIdParams } from "@simple-table/svelte";4 import { cellRendererConfig } from "./cell-renderer.demo-data";5 import type { CellRendererEmployee } from "./cell-renderer.demo-data";6 import CrTeamMembersCell from "./CrTeamMembersCell.svelte";7 import CrWebsiteCell from "./CrWebsiteCell.svelte";8 import CrStatusCell from "./CrStatusCell.svelte";9 import CrProgressCell from "./CrProgressCell.svelte";10 import CrRatingCell from "./CrRatingCell.svelte";11 import CrVerifiedCell from "./CrVerifiedCell.svelte";12 import CrTagsCell from "./CrTagsCell.svelte";13 import "@simple-table/svelte/styles.css";1415 let { height = "400px", theme }: { height?: string | number; theme?: Theme } = $props();1617 const RENDERERS: Record<string, unknown> = {18 teamMembers: CrTeamMembersCell,19 website: CrWebsiteCell,20 status: CrStatusCell,21 progress: CrProgressCell,22 rating: CrRatingCell,23 verified: CrVerifiedCell,24 tags: CrTagsCell,25 };2627 const headers: SvelteColumnDef<CellRendererEmployee>[] = cellRendererConfig.headers.map((h) => {28 const cellRenderer = RENDERERS[h.accessor as string];29 return cellRenderer ? { ...h, cellRenderer } : { ...h };30 });3132 const getRowId = ({ row }: GetRowIdParams<CellRendererEmployee>) => row.id;33</script>3435<SimpleTable36 columns={headers}37 rows={cellRendererConfig.rows}38 getRowId={getRowId}39 {height}40 {theme}41 selectableCells={cellRendererConfig.tableProps.selectableCells}42 customTheme={cellRendererConfig.tableProps.customTheme}43/>444546// CrProgressCell.svelte47<script lang="ts">48 import type { CellRendererProps } from "@simple-table/svelte";49 import type { CellRendererEmployee } from "./cell-renderer.demo-data";5051 let { value }: CellRendererProps<CellRendererEmployee> = $props();52 const pct = $derived(Number(value) || 0);53 const color = $derived(pct < 30 ? "#EF4444" : pct < 70 ? "#F59E0B" : "#10B981");54</script>5556<div>57 <div style="font-size:12px;margin-bottom:2px;">{pct}%</div>58 <div style="height:10px;background:#E5E7EB;border-radius:5px;overflow:hidden;">59 <div style="width:{pct}%;height:100%;background:{color};border-radius:5px;transition:width 0.3s;"></div>60 </div>61</div>626364// CrRatingCell.svelte65<script lang="ts">66 import type { CellRendererProps } from "@simple-table/svelte";67 import type { CellRendererEmployee } from "./cell-renderer.demo-data";6869 let { value }: CellRendererProps<CellRendererEmployee> = $props();70 const rating = $derived(Number(value) || 0);71 const full = $derived(Math.floor(rating));72 const hasHalf = $derived(rating % 1 >= 0.25);73 const empty = $derived(5 - full - (hasHalf ? 1 : 0));74</script>7576<span style="display:flex;align-items:center;gap:4px;">77 <span style="color:#F59E0B;letter-spacing:1px;">78 {"★".repeat(full)}{#if hasHalf}<span style="opacity:0.5;">★</span>{/if}{"☆".repeat(Math.max(0, empty))}79 </span>80 <span style="font-size:12px;color:#6b7280;">{rating}</span>81</span>828384// CrStatusCell.svelte85<script lang="ts">86 import type { CellRendererProps } from "@simple-table/svelte";87 import type { CellRendererEmployee } from "./cell-renderer.demo-data";8889 let { value }: CellRendererProps<CellRendererEmployee> = $props();90 const status = $derived(String(value));91 const meta = $derived.by(() => {92 const map: Record<string, { icon: string; color: string }> = {93 active: { icon: "✓", color: "#10B981" },94 inactive: { icon: "✕", color: "#EF4444" },95 pending: { icon: "!", color: "#F59E0B" },96 };97 return map[status] ?? { icon: "?", color: "#6b7280" };98 });99</script>100101<span style="color:{meta.color};font-weight:600;text-transform:capitalize;">{meta.icon} {status}</span>102103104// CrTagsCell.svelte105<script lang="ts">106 import type { CellRendererProps } from "@simple-table/svelte";107 import type { CellRendererEmployee } from "./cell-renderer.demo-data";108109 let { value }: CellRendererProps<CellRendererEmployee> = $props();110 const tags = $derived(Array.isArray(value) ? (value as string[]) : []);111</script>112113<div style="display:flex;gap:4px;flex-wrap:nowrap;overflow:hidden;">114 {#each tags as tag (tag)}115 <span116 style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500;background:#DBEAFE;color:#1E40AF;white-space:nowrap;"117 >{tag}</span>118 {/each}119</div>120121122// CrTeamMembersCell.svelte123<script lang="ts">124 import type { CellRendererProps } from "@simple-table/svelte";125 import type { CellRendererEmployee } from "./cell-renderer.demo-data";126127 let { row }: CellRendererProps<CellRendererEmployee> = $props();128 const members = $derived(row.teamMembers);129130 function initials(name: string): string {131 return name.split(" ").map((n) => n[0]).join("").toUpperCase();132 }133</script>134135<div style="display:flex;align-items:center;gap:6px;">136 {#each members as m (m.name)}137 <div style="display:flex;align-items:center;gap:4px;">138 <div139 style="width:24px;height:24px;border-radius:50%;background:#DBEAFE;color:#1E40AF;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0;"140 >141 {initials(m.name)}142 </div>143 <span style="font-size:13px;white-space:nowrap;">{m.name}</span>144 </div>145 {/each}146</div>147148149// CrVerifiedCell.svelte150<script lang="ts">151 import type { CellRendererProps } from "@simple-table/svelte";152 import type { CellRendererEmployee } from "./cell-renderer.demo-data";153154 let { value }: CellRendererProps<CellRendererEmployee> = $props();155 const yes = $derived(Boolean(value));156</script>157158<span style="color:{yes ? '#10B981' : '#EF4444'};font-weight:600;">{yes ? "✓ Yes" : "✕ No"}</span>159160161// CrWebsiteCell.svelte162<script lang="ts">163 import type { CellRendererProps } from "@simple-table/svelte";164 import type { CellRendererEmployee } from "./cell-renderer.demo-data";165166 let { value }: CellRendererProps<CellRendererEmployee> = $props();167 const url = $derived(String(value));168</script>169170<span>🌐 <a class="crw" href="https://{url}" target="_blank" rel="noopener noreferrer">{url}</a></span>171172<style>173 .crw {174 color: #2563eb;175 text-decoration: none;176 }177 .crw:hover {178 text-decoration: underline;179 }180</style>181
Solid TSX
Copy
1import { SimpleTable } from "@simple-table/solid";2import type {3 Theme,4 SolidColumnDef,5 SolidCellRenderer,6 CellRendererProps,7} from "@simple-table/solid";8import { cellRendererConfig } from "./cell-renderer.demo-data";9import type { CellRendererEmployee } from "./cell-renderer.demo-data";10import "@simple-table/solid/styles.css";1112const getInitials = (name: string) =>13 name14 .split(" ")15 .map((n) => n[0])16 .join("")17 .toUpperCase();1819const TeamCell = (props: CellRendererProps<CellRendererEmployee>) => {20 const members = props.row.teamMembers;21 return (22 <div style={{ display: "flex", "align-items": "center", gap: "6px" }}>23 {members.map((m) => (24 <div style={{ display: "flex", "align-items": "center", gap: "4px" }}>25 <div26 style={{27 width: "24px",28 height: "24px",29 "border-radius": "50%",30 background: "#DBEAFE",31 color: "#1E40AF",32 display: "flex",33 "align-items": "center",34 "justify-content": "center",35 "font-size": "10px",36 "font-weight": "600",37 "flex-shrink": "0",38 }}39 >40 {getInitials(m.name)}41 </div>42 <span style={{ "font-size": "13px", "white-space": "nowrap" }}>{m.name}</span>43 </div>44 ))}45 </div>46 );47};4849const WebsiteCell = (props: CellRendererProps<CellRendererEmployee>) => {50 const url = String(props.value);51 return (52 <span>53 🌐{" "}54 <a55 href={`https://${url}`}56 target="_blank"57 rel="noopener noreferrer"58 style={{ color: "#2563EB", "text-decoration": "none" }}59 onMouseEnter={(e) => (e.currentTarget.style.textDecoration = "underline")}60 onMouseLeave={(e) => (e.currentTarget.style.textDecoration = "none")}61 >62 {url}63 </a>64 </span>65 );66};6768const StatusCell = (props: CellRendererProps<CellRendererEmployee>) => {69 const status = String(props.value);70 const map: Record<string, { icon: string; color: string }> = {71 active: { icon: "✓", color: "#10B981" },72 inactive: { icon: "✕", color: "#EF4444" },73 pending: { icon: "!", color: "#F59E0B" },74 };75 const { icon, color } = map[status] ?? { icon: "?", color: "#6b7280" };76 return (77 <span style={{ color, "font-weight": "600", "text-transform": "capitalize" }}>78 {icon} {status}79 </span>80 );81};8283const ProgressCell = (props: CellRendererProps<CellRendererEmployee>) => {84 const pct = Number(props.value) || 0;85 const color = pct < 30 ? "#EF4444" : pct < 70 ? "#F59E0B" : "#10B981";86 return (87 <div>88 <div style={{ "font-size": "12px", "margin-bottom": "2px" }}>{pct}%</div>89 <div90 style={{91 height: "10px",92 background: "#E5E7EB",93 "border-radius": "5px",94 overflow: "hidden",95 }}96 >97 <div98 style={{99 width: `${pct}%`,100 height: "100%",101 background: color,102 "border-radius": "5px",103 transition: "width 0.3s",104 }}105 />106 </div>107 </div>108 );109};110111const RatingCell = (props: CellRendererProps<CellRendererEmployee>) => {112 const rating = Number(props.value) || 0;113 const full = Math.floor(rating);114 const hasHalf = rating % 1 >= 0.25;115 const empty = 5 - full - (hasHalf ? 1 : 0);116 return (117 <span style={{ display: "flex", "align-items": "center", gap: "4px" }}>118 <span style={{ color: "#F59E0B", "letter-spacing": "1px" }}>119 {"★".repeat(full)}120 {hasHalf && <span style={{ opacity: "0.5" }}>★</span>}121 {"☆".repeat(Math.max(0, empty))}122 </span>123 <span style={{ "font-size": "12px", color: "#6b7280" }}>{rating}</span>124 </span>125 );126};127128const VerifiedCell = (props: CellRendererProps<CellRendererEmployee>) => {129 const yes = Boolean(props.value);130 return (131 <span style={{ color: yes ? "#10B981" : "#EF4444", "font-weight": "600" }}>132 {yes ? "✓ Yes" : "✕ No"}133 </span>134 );135};136137const TagsCell = (props: CellRendererProps<CellRendererEmployee>) => {138 const raw = Array.isArray(props.value) ? props.value : [];139 const tags = raw.filter((t): t is string => typeof t === "string");140 return (141 <div style={{ display: "flex", gap: "4px", "flex-wrap": "nowrap", overflow: "hidden" }}>142 {tags.map((tag) => (143 <span144 style={{145 display: "inline-block",146 padding: "2px 8px",147 "border-radius": "4px",148 "font-size": "12px",149 "font-weight": "500",150 background: "#DBEAFE",151 color: "#1E40AF",152 "white-space": "nowrap",153 }}154 >155 {tag}156 </span>157 ))}158 </div>159 );160};161162const RENDERER_MAP: Partial<Record<string, SolidCellRenderer<CellRendererEmployee>>> = {163 teamMembers: TeamCell,164 website: WebsiteCell,165 status: StatusCell,166 progress: ProgressCell,167 rating: RatingCell,168 verified: VerifiedCell,169 tags: TagsCell,170};171172const HEADERS: SolidColumnDef<CellRendererEmployee>[] = cellRendererConfig.headers.map((h) => {173 const cellRenderer = RENDERER_MAP[String(h.accessor)];174 return cellRenderer ? { ...h, cellRenderer } : h;175});176177export default function CellRendererDemo(props: { height?: string | number; theme?: Theme }) {178 return (179 <SimpleTable180 columns={HEADERS}181 getRowId={({ row }) => row.id}182 rows={cellRendererConfig.rows}183 height={props.height ?? "400px"}184 theme={props.theme}185 selectableCells={cellRendererConfig.tableProps.selectableCells}186 customTheme={cellRendererConfig.tableProps.customTheme}187 />188 );189}
TypeScriptCellRendererDemo.ts
Copy
1import { SimpleTableVanilla } from "simple-table-core";2import type { Theme, ColumnDef, CellRenderer, CellRendererProps, GetRowIdParams } from "simple-table-core";3import { cellRendererConfig } from "./cell-renderer.demo-data";4import type { CellRendererEmployee } from "./cell-renderer.demo-data";5import "simple-table-core/styles.css";67const html = (str: string): Node => {8 const t = document.createElement("template");9 t.innerHTML = str.trim();10 return t.content;11};1213const getInitials = (name: string) =>14 name.split(" ").map((n) => n[0]).join("").toUpperCase();1516const RENDERERS: Record<string, CellRenderer<CellRendererEmployee>> = {17 teamMembers: ({ row }: CellRendererProps<CellRendererEmployee>) => {18 const members = row.teamMembers;19 return html(20 `<div style="display:flex;align-items:center;gap:6px">${members21 .map(22 (m) =>23 `<div style="display:flex;align-items:center;gap:4px"><div style="width:24px;height:24px;border-radius:50%;background:#DBEAFE;color:#1E40AF;display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;flex-shrink:0">${getInitials(m.name)}</div><span style="font-size:13px;white-space:nowrap">${m.name}</span></div>`,24 )25 .join("")}</div>`,26 );27 },2829 website: ({ value }) => {30 const url = String(value);31 return html(32 `<span>🌐 <a href="https://${url}" target="_blank" rel="noopener noreferrer" style="color:#2563EB;text-decoration:none" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">${url}</a></span>`,33 );34 },3536 status: ({ value }) => {37 const status = String(value);38 const map: Record<string, { icon: string; color: string }> = {39 active: { icon: "✓", color: "#10B981" },40 inactive: { icon: "✕", color: "#EF4444" },41 pending: { icon: "!", color: "#F59E0B" },42 };43 const { icon, color } = map[status] ?? { icon: "?", color: "#6b7280" };44 return html(45 `<span style="color:${color};font-weight:600;text-transform:capitalize">${icon} ${status}</span>`,46 );47 },4849 progress: ({ value }) => {50 const pct = Number(value) || 0;51 const color = pct < 30 ? "#EF4444" : pct < 70 ? "#F59E0B" : "#10B981";52 return html(53 `<div><div style="font-size:12px;margin-bottom:2px">${pct}%</div><div style="height:10px;background:#E5E7EB;border-radius:5px;overflow:hidden"><div style="width:${pct}%;height:100%;background:${color};border-radius:5px;transition:width 0.3s"></div></div></div>`,54 );55 },5657 rating: ({ value }) => {58 const rating = Number(value) || 0;59 const full = Math.floor(rating);60 const hasHalf = rating % 1 >= 0.25;61 const empty = 5 - full - (hasHalf ? 1 : 0);62 const halfStar = hasHalf ? '<span style="opacity:0.5">★</span>' : "";63 return html(64 `<span style="display:flex;align-items:center;gap:4px"><span style="color:#F59E0B;letter-spacing:1px">${"★".repeat(full)}${halfStar}${"☆".repeat(Math.max(0, empty))}</span><span style="font-size:12px;color:#6b7280">${rating}</span></span>`,65 );66 },6768 verified: ({ value }) => {69 const yes = Boolean(value);70 return html(71 `<span style="color:${yes ? "#10B981" : "#EF4444"};font-weight:600">${yes ? "✓ Yes" : "✕ No"}</span>`,72 );73 },7475 tags: ({ value }) => {76 const tags = Array.isArray(value) ? value.filter((tag): tag is string => typeof tag === "string") : [];77 return html(78 `<div style="display:flex;gap:4px;flex-wrap:nowrap;overflow:hidden">${tags79 .map(80 (tag) =>81 `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:12px;font-weight:500;background:#DBEAFE;color:#1E40AF;white-space:nowrap">${tag}</span>`,82 )83 .join("")}</div>`,84 );85 },86};878889const getRowId = ({ row }: GetRowIdParams<CellRendererEmployee>) => row.id;90export function renderCellRendererDemo(91 container: HTMLElement,92 options?: { height?: string | number; theme?: Theme },93): SimpleTableVanilla<CellRendererEmployee> {94 const headers: ColumnDef<CellRendererEmployee>[] = cellRendererConfig.headers.map((h) => {95 const renderer = RENDERERS[String(h.accessor)];96 return renderer ? { ...h, cellRenderer: renderer } : { ...h };97 });9899 const table = new SimpleTableVanilla(container, {100 getRowId,101 columns: headers,102 rows: cellRendererConfig.rows,103 height: options?.height ?? "400px",104 theme: options?.theme,105 selectableCells: cellRendererConfig.tableProps.selectableCells,106 customTheme: cellRendererConfig.tableProps.customTheme,107 });108 return table;109}110111112// cell-renderer.demo-data.ts113// Self-contained demo table setup for this example.114import type { ColumnDef } from "simple-table-core";115116117export type CellRendererEmployee = {118 id: number;119 name: string;120 website: string;121 status: string;122 progress: number;123 rating: number;124 verified: boolean;125 tags: string[];126 teamMembers: { name: string; role: string }[];127};128129export const cellRendererData: CellRendererEmployee[] = [130 { id: 1, name: "Isabella Romano", website: "isabellaromano.design", status: "active", progress: 92, rating: 4.9, verified: true, tags: ["UI/UX", "Design", "Frontend"], teamMembers: [{ name: "Alice Smith", role: "Designer" }, { name: "Bob Johnson", role: "Developer" }] },131 { id: 2, name: "Ethan McKenzie", website: "ethanmckenzie.dev", status: "active", progress: 87, rating: 4.7, verified: true, tags: ["Web Development", "Backend", "API"], teamMembers: [{ name: "Charlie Brown", role: "Backend Developer" }, { name: "Diana Prince", role: "Frontend Developer" }] },132 { id: 3, name: "Zoe Patterson", website: "zoepatterson.com", status: "pending", progress: 34, rating: 4.2, verified: false, tags: ["Branding", "Marketing"], teamMembers: [{ name: "Eve Adams", role: "Marketing Manager" }] },133 { id: 4, name: "Felix Chang", website: "felixchang.mobile", status: "active", progress: 95, rating: 4.8, verified: true, tags: ["Mobile App", "UX/UI"], teamMembers: [{ name: "Grace Lee", role: "UX Designer" }, { name: "Hank Johnson", role: "Mobile Developer" }] },134 { id: 5, name: "Aria Gonzalez", website: "ariagonzalez.writer", status: "active", progress: 78, rating: 4.6, verified: true, tags: ["Content Writing", "Copywriting"], teamMembers: [{ name: "Ivy White", role: "Content Strategist" }] },135 { id: 6, name: "Jasper Flynn", website: "jasperflynn.tech", status: "inactive", progress: 12, rating: 3.8, verified: false, tags: ["Consulting", "Tech Strategy"], teamMembers: [{ name: "Kate Brown", role: "Consultant" }] },136 { id: 7, name: "Nova Sterling", website: "novasterling.marketing", status: "active", progress: 83, rating: 4.5, verified: true, tags: ["Digital Marketing", "SEO"], teamMembers: [{ name: "Leo Wilson", role: "SEO Specialist" }, { name: "Mia Davis", role: "Marketing Analyst" }] },137 { id: 8, name: "Cruz Martinez", website: "cruzmartinez.photo", status: "active", progress: 71, rating: 4.4, verified: true, tags: ["Photography", "Videography"], teamMembers: [{ name: "Nina Smith", role: "Photographer" }, { name: "Owen Johnson", role: "Videographer" }] },138 { id: 9, name: "Sage Thompson", website: "sagethompson.ux", status: "active", progress: 89, rating: 4.7, verified: true, tags: ["UX Design", "UI Design"], teamMembers: [{ name: "Pete White", role: "UX Lead" }, { name: "Quinn Brown", role: "UI Designer" }] },139 { id: 10, name: "River Davis", website: "riverdavis.content", status: "pending", progress: 45, rating: 4.1, verified: false, tags: ["Content Strategy", "Copywriting"], teamMembers: [{ name: "Riley Adams", role: "Content Writer" }] },140 { id: 11, name: "Phoenix Williams", website: "phoenixwilliams.digital", status: "active", progress: 93, rating: 4.8, verified: true, tags: ["Digital Consulting", "Strategy"], teamMembers: [{ name: "Sofia Lee", role: "Consultant" }, { name: "Tucker Brown", role: "Digital Strategist" }] },141 { id: 12, name: "Atlas Johnson", website: "atlasjohnson.brand", status: "inactive", progress: 28, rating: 3.6, verified: false, tags: ["Brand Design", "Graphic Design"], teamMembers: [{ name: "Uma Patel", role: "Graphic Designer" }] },142];143144export const cellRendererHeaders: ColumnDef<CellRendererEmployee>[] = [145 { accessor: "id", label: "ID", width: 60, type: "number" },146 { accessor: "name", label: "Name", width: 180, type: "string" },147 { accessor: "teamMembers", label: "Team", width: 280, type: "string" },148 { accessor: "website", label: "Website", width: 180, type: "string" },149 { accessor: "status", label: "Status", width: 120, type: "string" },150 { accessor: "progress", label: "Progress", width: 150, type: "number" },151 { accessor: "rating", label: "Rating", width: 150, type: "number" },152 { accessor: "verified", label: "Verified", width: 100, type: "boolean" },153 { accessor: "tags", label: "Tags", width: 250, type: "string" },154];155156export const cellRendererConfig = {157 headers: cellRendererHeaders,158 rows: cellRendererData,159 tableProps: {160 selectableCells: true,161 customTheme: { rowHeight: 48 },162 },163};164
Props
Cell Renderer Configuration
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
ColumnDef.cellRenderer | Optional | Custom cell content. Framework adapters accept components or render functions; vanilla returns string/number/null or a DOM Node. In Angular, prefer an stCell template on the page; a matching template wins over cellRenderer on that column. |
Renderer arguments
CellRendererProps
| Property | Required | Description | Example |
|---|---|---|---|
Property | Required | Description | Example |
accessor | Required | The column accessor/key for the cell being rendered | |
colIndexnumber | Required | The column index (0-based) | |
row | Required | The complete row object containing all data for this row | |
rowIndexnumber | Required | The row index (0-based) | |
rowPath(string | number)[] | Optional | Array path through the nested data structure to reach this row. Each element is either a number (array index) or string (property name). Useful for accessing nested/hierarchical data. | |
theme | Required | Current theme of the table | |
value | Required | The raw cell value | |
formattedValuestring | number | string[] | number[] | boolean | null | undefined | Optional | The formatted cell value (output from valueFormatter if defined). Use this for display purposes when you need both raw and formatted values. |