Buttons, links and images in cells
Built-in button, link and image cells: clicks do not select the row or start an edit, links take {field} templates and refuse javascript: URLs, and renderers can clean up with destroy.
Live demoButtons, links and images
Open and delete buttons, profile links and photos.
Code for this demo
import { createGrid, type ColumnDef, type Row } from '@freegridjs/core';
import { makeRows } from '../data';
import { button, separator, text, tr, type DemoContext } from '../ui';
export default function mount(ctx: DemoContext) {
const h = (en: string, ko: string) => tr(ctx.lang, { en, ko });
const photo = (i: number) => `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="hsl(${(i * 47) % 360} 70% 55%)"/><text x="16" y="21" font-size="14" text-anchor="middle" fill="white" font-family="sans-serif">${String.fromCharCode(65 + (i % 26))}</text></svg>`)}`;
const data = makeRows(200).map((r, i) => ({ ...r, photo: photo(i), archived: i % 5 === 0, locked: i % 11 === 0 }));
const grid = createGrid(ctx.host, {
columns: [
{ field: 'photo', header: '', width: 52, image: { width: 28, height: 28, round: true, alt: p => p.row.name } },
{ field: 'name', header: h('Name', '이름'), width: 170, link: { href: '#/people/{id}', onClick: p => ctx.log(`open profile #${p.row.id}`) } },
{ field: 'email', header: h('Email', '이메일'), width: 240, link: { href: p => `mailto:${p.value}` } },
{ id: 'open', header: '', width: 90, button: { label: h('Open', '열기'), onClick: p => ctx.log(`open order #${p.row.id}`) } },
{ id: 'archive', header: '', width: 100, button: {
label: p => (p.row.archived ? h('Restore', '복원') : h('Archive', '보관')),
disabled: p => p.row.locked,
onClick: p => { grid.applyTransaction({ update: [{ ...p.row, archived: !p.row.archived }] }); ctx.log(`#${p.row.id} archived = ${!p.row.archived}`); },
} },
],
data, locale: ctx.lang, getRowId: r => String(r.id), rowSelection: 'multiple',
});
text(ctx, h('Buttons and links act on their own: the row is not selected and no editor opens. Locked rows have a greyed-out button.',
'버튼과 링크는 따로 동작해 행이 선택되거나 편집기가 열리지 않습니다. 잠긴 행의 버튼은 흐리게 보입니다.'));
return grid;
}Three column properties put a clickable element in every cell without writing a renderer. Their clicks belong to them: the focus does not move, the row is not selected or checked, a range does not start and a double-click does not open an editor.
Buttons
const columns = [
{ id: 'open', header: '', width: 80, pinned: 'right',
button: { label: 'Open', onClick: p => openOrder(p.row.id) } },
{ id: 'remove', header: '', width: 80,
button: {
label: p => (p.row.archived ? 'Restore' : 'Archive'), // text per row
disabled: p => p.row.locked, // greyed out, cannot be clicked
onClick: p => grid.applyTransaction({ update: [{ ...p.row, archived: !p.row.archived }] }),
} },
];| Name | Type | Description |
|---|---|---|
label | string | (params) => string | The button text (default: the cell value). |
onClick | (params) => void | Called with { value, formatted, row, rowIndex, column }. |
disabled | (params) => boolean | Grey the button out for some rows. |
Links
const columns = [
// a template: {field} is replaced by the row's value (URL-encoded)
{ field: 'name', link: { href: '/customers/{id}?tab=orders' } },
// a function, a new tab, a fixed label
{ field: 'site', link: { href: p => p.value, target: '_blank', label: () => 'Visit' } },
{ field: 'email', link: { href: p => 'mailto:' + p.value } },
// handled in your app (a router) instead of a page load
{ field: 'orderNo', link: { href: '/orders/{orderNo}', onClick: (p, e) => router.push('/orders/' + p.row.orderNo) } },
];| Name | Type | Description |
|---|---|---|
href | string | (params) => string | A template with {field} placeholders, or a function. Only http(s), mailto:, tel: and relative links (/, ./, #, ?) are made; anything else (javascript:) leaves the text without a link. |
target | string | Where to open it; '_blank' also gets rel="noopener noreferrer". |
label | (params) => string | The link text (default: the formatted value). |
onClick | (params, event) => void | Handle the click yourself; the browser does not follow the link. |
Images
{ field: 'photoUrl', header: 'Photo', width: 60, image: { width: 32, height: 32, round: true, alt: p => p.row.name } }| Name | Type | Description |
|---|---|---|
width / height | number | Size in px (height defaults to the row height less 8). |
round | boolean | A circle (profile pictures). |
alt | (params) => string | Text for screen readers. |
Images load lazily as rows scroll into view. Only http(s), data:image/, blob: and relative addresses are shown. Excel export can embed them with images: true (see Excel: pictures, notes, protection).
Your own elements: cleaning up
For anything else, a renderer returns HTML or an element. If it mounts something that needs cleaning up (a React root, a chart, a timer), return { el, destroy }: destroy runs before the cell is drawn again and when the grid is destroyed.
import { createRoot } from 'react-dom/client';
{ field: 'trend', renderer: p => {
const el = document.createElement('div');
const root = createRoot(el);
root.render(<TrendChart values={p.value} />);
return { el, destroy: () => root.unmount() };
} }Questions about this page
How do I add a button to each row of a grid?
Add a column with button: { label: 'Open', onClick: p => open(p.row) }. The click calls onClick with the cell and does not select the row or open an editor.