Advanced Tables with Calculated Columns
This example demonstrates advanced table features including automatic calculations. It shows how to create a table with calculated columns that automatically update when values change.
Features
- Automatic Calculations: Quantity × Price = Total for each row
- Grand Total: Automatically calculated sum of all totals
- Real-time Updates: Calculations update immediately when you change quantity or price values
- Split cells: Merge and split table cells
- Cell background color: Color individual cells
- Cell text color: Change text color in cells
- Table row and column headers: Use headers for better organization
How It Works
The example uses the onChange
event listener to detect when table content changes. When a table is updated, it automatically:
- Extracts quantity and price values from each data row
- Calculates the total (quantity × price) for each row
- Updates the total column with the calculated values
- Calculates and updates the grand total
Code Highlights
<BlockNoteView
editor={editor}
onChange={(editor, { getChanges }) => {
const changes = getChanges();
changes.forEach((change) => {
if (change.type === "update" && change.block.type === "table") {
const updatedRows = calculateTableTotals(change.block);
if (updatedRows) {
editor.updateBlock(change.block, {
type: "table",
content: {
...change.block.content,
rows: updatedRows as any,
} as any,
});
}
}
});
}}
></BlockNoteView>
Relevant Docs:
import "@blocknote/core/fonts/inter.css";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import { useCreateBlockNote } from "@blocknote/react";
import type { Block, DefaultBlockSchema } from "@blocknote/core";
import { useRef } from "react";
export default function App() {
const applying = useRef(false);
// Creates a new editor instance.
const editor = useCreateBlockNote({
// This enables the advanced table features
tables: {
splitCells: true,
cellBackgroundColor: true,
cellTextColor: true,
headers: true,
},
initialContent: [
{
id: "7e498b3d-d42e-4ade-9be0-054b292715ea",
type: "heading",
props: {
textColor: "default",
backgroundColor: "default",
textAlignment: "left",
level: 2,
},
content: [
{
type: "text",
text: "Advanced Tables with Calculated Columns",
styles: {},
},
],
children: [],
},
{
id: "cbf287c6-770b-413a-bff5-ad490a0b562a",
type: "table",
props: {
textColor: "default",
},
content: {
type: "tableContent",
columnWidths: [150, 120, 120, 120],
headerRows: 1,
rows: [
{
cells: [
{
type: "tableCell",
content: [
{
type: "text",
text: "Item",
styles: { bold: true },
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "gray",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "Quantity",
styles: { bold: true },
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "gray",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "Price ($)",
styles: { bold: true },
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "gray",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "Total ($)",
styles: { bold: true },
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "blue",
textColor: "white",
textAlignment: "center",
},
},
],
},
{
cells: [
{
type: "tableCell",
content: [
{
type: "text",
text: "Laptop",
styles: {},
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "default",
textColor: "default",
textAlignment: "left",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "2",
styles: {},
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "default",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "1200",
styles: {},
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "default",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "2400",
styles: { bold: true },
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "green",
textColor: "white",
textAlignment: "center",
},
},
],
},
{
cells: [
{
type: "tableCell",
content: [
{
type: "text",
text: "Mouse",
styles: {},
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "default",
textColor: "default",
textAlignment: "left",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "5",
styles: {},
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "default",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "25",
styles: {},
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "default",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "125",
styles: { bold: true },
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "green",
textColor: "white",
textAlignment: "center",
},
},
],
},
{
cells: [
{
type: "tableCell",
content: [
{
type: "text",
text: "Keyboard",
styles: {},
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "default",
textColor: "default",
textAlignment: "left",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "3",
styles: {},
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "default",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "80",
styles: {},
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "default",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "240",
styles: { bold: true },
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "green",
textColor: "white",
textAlignment: "center",
},
},
],
},
{
cells: [
{
type: "tableCell",
content: [
{
type: "text",
text: "Grand Total",
styles: { bold: true },
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "yellow",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "",
styles: {},
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "yellow",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "",
styles: {},
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "yellow",
textColor: "default",
textAlignment: "center",
},
},
{
type: "tableCell",
content: [
{
type: "text",
text: "2765",
styles: { bold: true },
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "red",
textColor: "white",
textAlignment: "center",
},
},
],
},
],
},
children: [],
},
{
id: "16e76a94-74e5-42e2-b461-fc9da9f381f7",
type: "paragraph",
props: {
textColor: "default",
backgroundColor: "default",
textAlignment: "left",
},
content: [
{
type: "text",
text: "Features:",
styles: {},
},
],
children: [
{
id: "785fc9f7-8554-47f4-a4df-8fe2f1438cac",
type: "bulletListItem",
props: {
textColor: "default",
backgroundColor: "default",
textAlignment: "left",
},
content: [
{
type: "text",
text: "Automatic calculation of totals (Quantity × Price)",
styles: {},
},
],
children: [],
},
{
id: "1d0adf08-1b42-421a-b9ea-b3125dcc96d9",
type: "bulletListItem",
props: {
textColor: "default",
backgroundColor: "default",
textAlignment: "left",
},
content: [
{
type: "text",
text: "Grand total calculation",
styles: {},
},
],
children: [],
},
{
id: "99991aa7-9d86-4d06-9073-b1a9c0329062",
type: "bulletListItem",
props: {
textColor: "default",
backgroundColor: "default",
textAlignment: "left",
},
content: [
{
type: "text",
text: "Cell background & foreground coloring",
styles: {},
},
],
children: [],
},
{
id: "c7bf2a7c-8972-44f1-acd8-cf60fa734068",
type: "bulletListItem",
props: {
textColor: "default",
backgroundColor: "default",
textAlignment: "left",
},
content: [
{
type: "text",
text: "Splitting & merging cells",
styles: {},
},
],
children: [],
},
{
id: "785fc9f7-8554-47f4-a4df-8fe2f1438cac",
type: "bulletListItem",
props: {
textColor: "default",
backgroundColor: "default",
textAlignment: "left",
},
content: [
{
type: "text",
text: "Header rows & columns",
styles: {},
},
],
children: [],
},
],
},
{
id: "c7bf2a7c-8972-44f1-acd8-cf60fa734068",
type: "paragraph",
props: {
textColor: "default",
backgroundColor: "default",
textAlignment: "left",
},
content: [],
children: [],
},
],
});
// Function to calculate totals for a table
const calculateTableTotals = (tableBlock: Block<DefaultBlockSchema>) => {
if (tableBlock.type !== "table") return;
const rows = tableBlock.content.rows;
if (rows.length < 2) return; // Need at least header + 1 data row
let grandTotal = 0;
const updatedRows = rows.map((row, rowIndex: number) => {
if (rowIndex === 0) return row; // Skip header row
if (rowIndex === rows.length - 1) return row; // Skip grand total row
// Helper function to extract text from a cell
const getCellText = (cell: any): string => {
if (typeof cell === "string") return cell;
if (cell && typeof cell === "object" && "content" in cell) {
return cell.content?.[0]?.text || "0";
}
return "0";
};
const itemText = getCellText(row.cells[0]);
const quantityText = getCellText(row.cells[1]);
const priceText = getCellText(row.cells[2]);
const quantity = parseFloat(quantityText) || 0;
const price = parseFloat(priceText) || 0;
const total = quantity * price;
grandTotal += total;
// Update the total cell
const updatedCells = [...row.cells];
updatedCells[3] = {
type: "tableCell",
content: [
{
type: "text",
text: total.toString(),
styles: { bold: true },
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "green",
textColor: "white",
textAlignment: "center",
},
};
// Update item label if total is above 4k
const baseItemText = itemText.replace(" (eligible for discount)", "");
if (total >= 4000) {
updatedCells[0] = {
...row.cells[0],
content: [
{
type: "text",
text: baseItemText + " (eligible for discount)",
styles: {},
},
],
};
} else {
updatedCells[0] = {
...row.cells[0],
content: [
{
type: "text",
text: baseItemText,
styles: {},
},
],
};
}
return {
...row,
cells: updatedCells,
};
});
// Update grand total row
const grandTotalRow = updatedRows[rows.length - 1];
if (grandTotalRow) {
const updatedGrandTotalCells = [...grandTotalRow.cells];
updatedGrandTotalCells[3] = {
type: "tableCell",
content: [
{
type: "text",
text: grandTotal.toString(),
styles: { bold: true },
},
],
props: {
colspan: 1,
rowspan: 1,
backgroundColor: "red",
textColor: "white",
textAlignment: "center",
},
};
updatedRows[rows.length - 1] = {
...grandTotalRow,
cells: updatedGrandTotalCells,
};
}
return updatedRows as typeof tableBlock.content.rows;
};
// Renders the editor instance using a React component.
return (
<BlockNoteView
editor={editor}
onChange={(editor, { getChanges }) => {
const changes = getChanges();
if (changes.length === 0 || applying.current) return;
// prevents a double onChange because we're updating the block here
applying.current = true;
changes.forEach((change) => {
if (change.type === "update" && change.block.type === "table") {
const updatedRows = calculateTableTotals(change.block);
if (updatedRows) {
// Use any type to bypass complex type checking for this demo
editor.updateBlock(change.block, {
type: "table",
content: {
...change.block.content,
rows: updatedRows,
},
});
}
}
});
requestAnimationFrame(() => (applying.current = false));
}}
></BlockNoteView>
);
}