🚀 BlockNote AI is here! Access the early preview.

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:

  1. Extracts quantity and price values from each data row
  2. Calculates the total (quantity × price) for each row
  3. Updates the total column with the calculated values
  4. 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>
  );
}