Skip to main content

Overview

Table blocks provide a structured way to organize data in rows and columns. They support dynamic resizing, cell selection, row/column management, and rich text content within cells.

Block Keys

Table Block Keys

class TableBlockKeys {
  static const String type = 'table';
  static const String colDefaultWidth = 'colDefaultWidth';
  static const String rowDefaultHeight = 'rowDefaultHeight';
  static const String colMinimumWidth = 'colMinimumWidth';
  static const String borderWidth = 'borderWidth';
  static const String colsLen = 'colsLen';      // Number of columns
  static const String rowsLen = 'rowsLen';      // Number of rows
  static const String colsHeight = 'colsHeight';
}

Table Cell Keys

class TableCellBlockKeys {
  static const String type = 'table/cell';
  static const String rowPosition = 'rowPosition';
  static const String colPosition = 'colPosition';
  static const String height = 'height';
  static const String width = 'width';
  static const String rowBackgroundColor = 'rowBackgroundColor';
  static const String colBackgroundColor = 'colBackgroundColor';
}
Source: /lib/src/editor/block_component/table_block_component/table_block_component.dart:7

Table Structure

Tables are composed of:
  • Table Node - Container with table attributes
  • Cell Nodes - Individual cells with position data
  • Cell Content - Paragraph nodes inside each cell
table (colsLen: 2, rowsLen: 2)
├─ cell (row: 0, col: 0)
│  └─ paragraph
├─ cell (row: 0, col: 1)
│  └─ paragraph
├─ cell (row: 1, col: 0)
│  └─ paragraph
└─ cell (row: 1, col: 1)
   └─ paragraph

Creating Tables

Using TableNode Helper

// Create a 2x2 table
final table = TableNode.fromList([
  ['Cell 1,1', 'Cell 1,2'],
  ['Cell 2,1', 'Cell 2,2'],
]);

// Access the node
final tableNode = table.node;

Creating Table Cells

Node tableCellNode(String text, int rowPosition, int colPosition) {
  return Node(
    type: TableCellBlockKeys.type,
    attributes: {
      TableCellBlockKeys.rowPosition: rowPosition,
      TableCellBlockKeys.colPosition: colPosition,
    },
    children: [
      paragraphNode(text: text),
    ],
  );
}
Source: /lib/src/editor/block_component/table_block_component/table_cell_block_component.dart:30

Manual Table Creation

final tableNode = Node(
  type: TableBlockKeys.type,
  attributes: {
    TableBlockKeys.colsLen: 3,
    TableBlockKeys.rowsLen: 2,
    TableBlockKeys.colDefaultWidth: 160.0,
    TableBlockKeys.rowDefaultHeight: 40.0,
  },
  children: [
    // Row 0
    tableCellNode('A1', 0, 0),
    tableCellNode('B1', 0, 1),
    tableCellNode('C1', 0, 2),
    // Row 1
    tableCellNode('A2', 1, 0),
    tableCellNode('B2', 1, 1),
    tableCellNode('C2', 1, 2),
  ],
);

Table Styling

TableStyle Configuration

class TableStyle {
  final double colWidth;
  final double rowHeight;
  final double colMinimumWidth;
  final double borderWidth;
  final Widget addIcon;
  final Widget handlerIcon;
  final Color borderColor;
  final Color borderHoverColor;

  const TableStyle({
    this.colWidth = 160,
    this.rowHeight = 40,
    this.colMinimumWidth = 40,
    this.borderWidth = 2,
    this.addIcon = TableDefaults.addIcon,
    this.handlerIcon = TableDefaults.handlerIcon,
    this.borderColor = TableDefaults.borderColor,
    this.borderHoverColor = TableDefaults.borderHoverColor,
  });
}
Default Values:
  • Column Width: 160px
  • Row Height: 40px
  • Minimum Column Width: 40px
  • Border Width: 2px
  • Border Color: Grey
  • Border Hover Color: Blue
Source: /lib/src/editor/block_component/table_block_component/table_block_component.dart:27

Component Builder

Create a table component with custom styling:
final tableBuilder = TableBlockComponentBuilder(
  configuration: BlockComponentConfiguration(
    padding: (node) => const EdgeInsets.all(8.0),
  ),
  tableStyle: const TableStyle(
    colWidth: 200,
    rowHeight: 50,
    borderColor: Colors.grey,
    borderHoverColor: Colors.blue,
  ),
  menuBuilder: (node, editorState, index, direction, onBuild, onClose) {
    // Custom menu for row/column operations
    return TableContextMenu(
      node: node,
      index: index,
      direction: direction,
      onDelete: () => deleteRowOrColumn(node, index, direction),
      onInsert: () => insertRowOrColumn(node, index, direction),
    );
  },
);
Source: /lib/src/editor/block_component/table_block_component/table_block_component.dart:80

Using in Editor

Add table support to your editor:
AppFlowyEditor(
  editorState: editorState,
  blockComponentBuilders: {
    TableBlockKeys.type: TableBlockComponentBuilder(
      tableStyle: const TableStyle(
        colWidth: 180,
        rowHeight: 45,
      ),
    ),
    TableCellBlockKeys.type: TableCellBlockComponentBuilder(
      configuration: standardBlockComponentConfiguration,
    ),
    // ... other block types
  },
);

Inserting Tables

Using Slash Menu

final tableMenuItem = SelectionMenuItem(
  getName: () => 'Table',
  icon: (editorState, isSelected, style) => SelectionMenuIconWidget(
    icon: Icons.table_view,
    isSelected: isSelected,
    style: style,
  ),
  keywords: ['table'],
  handler: (editorState, _, __) {
    final selection = editorState.selection;
    if (selection == null || !selection.isCollapsed) return;

    final currentNode = editorState.getNodeAtPath(selection.end.path);
    if (currentNode == null) return;

    // Create 2x2 table
    final tableNode = TableNode.fromList([
      ['', ''],
      ['', ''],
    ]);

    final transaction = editorState.transaction;
    final delta = currentNode.delta;
    
    if (delta != null && delta.isEmpty) {
      // Replace empty block
      transaction
        ..insertNode(selection.end.path, tableNode.node)
        ..deleteNode(currentNode);
      transaction.afterSelection = Selection.collapsed(
        Position(
          path: selection.end.path + [0, 0],
          offset: 0,
        ),
      );
    } else {
      // Insert after current block
      transaction.insertNode(selection.end.path.next, tableNode.node);
      transaction.afterSelection = Selection.collapsed(
        Position(
          path: selection.end.path.next + [0, 0],
          offset: 0,
        ),
      );
    }

    editorState.apply(transaction);
  },
);
Source: /lib/src/editor/block_component/table_block_component/table_block_component.dart:345

Programmatic Insertion

// Insert table at path
void insertTable(List<int> path, int rows, int cols) {
  final cells = List.generate(
    rows,
    (row) => List.generate(cols, (col) => ''),
  );
  
  final tableNode = TableNode.fromList(cells);
  
  final transaction = editorState.transaction;
  transaction.insertNode(path, tableNode.node);
  editorState.apply(transaction);
}

// Usage:
insertTable([0], 3, 4); // Insert 3x4 table at position 0

Table Operations

Add Row

void addRow(Node tableNode, int afterIndex) {
  final colsLen = tableNode.attributes[TableBlockKeys.colsLen] as int;
  final rowsLen = tableNode.attributes[TableBlockKeys.rowsLen] as int;
  
  final transaction = editorState.transaction;
  
  // Update table attributes
  transaction.updateNode(tableNode, {
    TableBlockKeys.rowsLen: rowsLen + 1,
  });
  
  // Add cells for new row
  final newRowIndex = afterIndex + 1;
  for (var col = 0; col < colsLen; col++) {
    final cellIndex = newRowIndex * colsLen + col;
    transaction.insertNode(
      [...tableNode.path, cellIndex],
      tableCellNode('', newRowIndex, col),
    );
  }
  
  // Update positions of cells after the inserted row
  for (var row = newRowIndex + 1; row <= rowsLen; row++) {
    for (var col = 0; col < colsLen; col++) {
      final cell = getCellNode(tableNode, col, row);
      if (cell != null) {
        transaction.updateNode(cell, {
          TableCellBlockKeys.rowPosition: row,
        });
      }
    }
  }
  
  editorState.apply(transaction);
}

Add Column

void addColumn(Node tableNode, int afterIndex) {
  final colsLen = tableNode.attributes[TableBlockKeys.colsLen] as int;
  final rowsLen = tableNode.attributes[TableBlockKeys.rowsLen] as int;
  
  final transaction = editorState.transaction;
  
  // Update table attributes
  transaction.updateNode(tableNode, {
    TableBlockKeys.colsLen: colsLen + 1,
  });
  
  // Add cells for new column
  final newColIndex = afterIndex + 1;
  for (var row = 0; row < rowsLen; row++) {
    transaction.insertNode(
      [...tableNode.path, (row * (colsLen + 1)) + newColIndex],
      tableCellNode('', row, newColIndex),
    );
  }
  
  // Update positions of cells after the inserted column
  for (var row = 0; row < rowsLen; row++) {
    for (var col = newColIndex + 1; col <= colsLen; col++) {
      final cell = getCellNode(tableNode, col, row);
      if (cell != null) {
        transaction.updateNode(cell, {
          TableCellBlockKeys.colPosition: col,
        });
      }
    }
  }
  
  editorState.apply(transaction);
}

Delete Row

void deleteRow(Node tableNode, int rowIndex) {
  final colsLen = tableNode.attributes[TableBlockKeys.colsLen] as int;
  final rowsLen = tableNode.attributes[TableBlockKeys.rowsLen] as int;
  
  if (rowsLen <= 1) return; // Don't delete last row
  
  final transaction = editorState.transaction;
  
  // Delete cells in row
  for (var col = 0; col < colsLen; col++) {
    final cell = getCellNode(tableNode, col, rowIndex);
    if (cell != null) {
      transaction.deleteNode(cell);
    }
  }
  
  // Update table attributes
  transaction.updateNode(tableNode, {
    TableBlockKeys.rowsLen: rowsLen - 1,
  });
  
  // Update positions of cells after deleted row
  for (var row = rowIndex + 1; row < rowsLen; row++) {
    for (var col = 0; col < colsLen; col++) {
      final cell = getCellNode(tableNode, col, row);
      if (cell != null) {
        transaction.updateNode(cell, {
          TableCellBlockKeys.rowPosition: row - 1,
        });
      }
    }
  }
  
  editorState.apply(transaction);
}

Delete Column

void deleteColumn(Node tableNode, int colIndex) {
  final colsLen = tableNode.attributes[TableBlockKeys.colsLen] as int;
  final rowsLen = tableNode.attributes[TableBlockKeys.rowsLen] as int;
  
  if (colsLen <= 1) return; // Don't delete last column
  
  final transaction = editorState.transaction;
  
  // Delete cells in column
  for (var row = 0; row < rowsLen; row++) {
    final cell = getCellNode(tableNode, colIndex, row);
    if (cell != null) {
      transaction.deleteNode(cell);
    }
  }
  
  // Update table attributes
  transaction.updateNode(tableNode, {
    TableBlockKeys.colsLen: colsLen - 1,
  });
  
  // Update positions of cells after deleted column
  for (var row = 0; row < rowsLen; row++) {
    for (var col = colIndex + 1; col < colsLen; col++) {
      final cell = getCellNode(tableNode, col, row);
      if (cell != null) {
        transaction.updateNode(cell, {
          TableCellBlockKeys.colPosition: col - 1,
        });
      }
    }
  }
  
  editorState.apply(transaction);
}

Cell Styling

Background Colors

// Set row background color
void setRowBackgroundColor(Node tableNode, int rowIndex, Color color) {
  final colsLen = tableNode.attributes[TableBlockKeys.colsLen] as int;
  final transaction = editorState.transaction;
  
  for (var col = 0; col < colsLen; col++) {
    final cell = getCellNode(tableNode, col, rowIndex);
    if (cell != null) {
      transaction.updateNode(cell, {
        TableCellBlockKeys.rowBackgroundColor: 
            '0x${color.value.toRadixString(16)}',
      });
    }
  }
  
  editorState.apply(transaction);
}

// Set column background color
void setColumnBackgroundColor(Node tableNode, int colIndex, Color color) {
  final rowsLen = tableNode.attributes[TableBlockKeys.rowsLen] as int;
  final transaction = editorState.transaction;
  
  for (var row = 0; row < rowsLen; row++) {
    final cell = getCellNode(tableNode, colIndex, row);
    if (cell != null) {
      transaction.updateNode(cell, {
        TableCellBlockKeys.colBackgroundColor: 
            '0x${color.value.toRadixString(16)}',
      });
    }
  }
  
  editorState.apply(transaction);
}
Source: /lib/src/editor/block_component/table_block_component/table_cell_block_component.dart:119

Custom Cell Colors

TableCellBlockComponentBuilder(
  colorBuilder: (context, node) {
    // Custom color logic
    final rowPos = node.attributes[TableCellBlockKeys.rowPosition] as int;
    
    // Alternate row colors
    if (rowPos % 2 == 0) {
      return Colors.grey[50];
    }
    return Colors.white;
  },
)

Column Resizing

// Update column width
void setColumnWidth(Node tableNode, int colIndex, double width) {
  final rowsLen = tableNode.attributes[TableBlockKeys.rowsLen] as int;
  final transaction = editorState.transaction;
  
  for (var row = 0; row < rowsLen; row++) {
    final cell = getCellNode(tableNode, colIndex, row);
    if (cell != null) {
      transaction.updateNode(cell, {
        TableCellBlockKeys.width: width,
      });
    }
  }
  
  editorState.apply(transaction);
}

Row Height

// Update row height
void setRowHeight(Node tableNode, int rowIndex, double height) {
  final colsLen = tableNode.attributes[TableBlockKeys.colsLen] as int;
  final transaction = editorState.transaction;
  
  for (var col = 0; col < colsLen; col++) {
    final cell = getCellNode(tableNode, col, rowIndex);
    if (cell != null) {
      transaction.updateNode(cell, {
        TableCellBlockKeys.height: height,
      });
    }
  }
  
  editorState.apply(transaction);
}

Cell Navigation

// Navigate to specific cell
void navigateToCell(Node tableNode, int row, int col) {
  final cell = getCellNode(tableNode, col, row);
  if (cell != null && cell.children.isNotEmpty) {
    final paragraphPath = [...cell.path, 0];
    editorState.updateSelection(
      Selection.collapsed(
        Position(path: paragraphPath, offset: 0),
      ),
    );
  }
}

// Navigate to next cell (Tab behavior)
void navigateToNextCell(Node currentCell) {
  final tableNode = currentCell.parent;
  if (tableNode == null) return;
  
  final colPos = currentCell.attributes[TableCellBlockKeys.colPosition] as int;
  final rowPos = currentCell.attributes[TableCellBlockKeys.rowPosition] as int;
  final colsLen = tableNode.attributes[TableBlockKeys.colsLen] as int;
  final rowsLen = tableNode.attributes[TableBlockKeys.rowsLen] as int;
  
  int nextCol = colPos + 1;
  int nextRow = rowPos;
  
  if (nextCol >= colsLen) {
    nextCol = 0;
    nextRow++;
  }
  
  if (nextRow < rowsLen) {
    navigateToCell(tableNode, nextRow, nextCol);
  }
}

Table Commands

AppFlowy Editor includes built-in table commands:
// Enable table commands
commandShortcutEvents: [
  ...tableCommands,
  // ... other commands
],
Included Commands:
  • Tab - Navigate to next cell
  • Shift+Tab - Navigate to previous cell
  • Enter - New line in cell
  • Backspace - Delete in cell (special handling)

Validation

Table validation ensures structure integrity:
BlockComponentValidate get validate => (node) {
  // Check node has attributes
  if (node.attributes.isEmpty) return false;

  // Check required attributes
  if (!node.attributes.containsKey(TableBlockKeys.colsLen) ||
      !node.attributes.containsKey(TableBlockKeys.rowsLen)) {
    return false;
  }

  final colsLen = node.attributes[TableBlockKeys.colsLen];
  final rowsLen = node.attributes[TableBlockKeys.rowsLen];
  final children = node.children;

  // Check children count matches dimensions
  if (children.length != colsLen * rowsLen) return false;

  // Verify all cells have correct positions
  for (var i = 0; i < colsLen; i++) {
    for (var j = 0; j < rowsLen; j++) {
      final child = children.where(
        (n) =>
            n.attributes[TableCellBlockKeys.colPosition] == i &&
            n.attributes[TableCellBlockKeys.rowPosition] == j,
      );
      if (child.isEmpty || child.length != 1) return false;
    }
  }

  return true;
};
Source: /lib/src/editor/block_component/table_block_component/table_block_component.dart:118

Scrollable Tables

Tables are wrapped in a horizontal scrollbar:
Scrollbar(
  controller: _scrollController,
  child: SingleChildScrollView(
    padding: const EdgeInsets.only(top: 10, left: 10, bottom: 4),
    controller: _scrollController,
    scrollDirection: Axis.horizontal,
    child: TableView(
      tableNode: widget.tableNode,
      editorState: editorState,
      menuBuilder: widget.menuBuilder,
      tableStyle: widget.tableStyle,
    ),
  ),
)
Source: /lib/src/editor/block_component/table_block_component/table_block_component.dart:230

Key Features

  • Dynamic Sizing - Resizable columns and rows
  • Rich Text Cells - Full paragraph formatting in cells
  • Row/Column Operations - Add, delete, reorder
  • Cell Styling - Background colors per row/column
  • Keyboard Navigation - Tab, arrow keys, shortcuts
  • Selection Support - Block and text selection
  • Scrollable - Horizontal scroll for wide tables
  • Validation - Ensures structural integrity
  • Custom Menus - Context menus for operations

Best Practices

  1. Start small - Begin with 2-3 columns
  2. Keep content concise - Tables are for structured data
  3. Use headers - Style first row as header
  4. Consistent widths - Maintain uniform column widths
  5. Adequate spacing - Don’t crowd content
  6. Test navigation - Ensure keyboard navigation works
  7. Consider mobile - Tables may not render well on small screens

Accessibility

  • Keyboard navigation fully supported
  • Screen readers can navigate table structure
  • Proper semantic roles for table elements
  • Selection indicators are clear

Custom Blocks

Create custom block components

Paragraph Blocks

Cell content uses paragraphs

Build docs developers (and LLMs) love