Skip to main content
Transactions are the recommended way to modify documents in AppFlowy Editor. They ensure consistency, enable undo/redo functionality, and support collaborative editing.

What is a Transaction?

A Transaction is a collection of operations that will be applied to the document atomically. All document mutations should go through transactions.
class Transaction {
  final Document document;
  final List<Operation> operations;
  
  Selection? beforeSelection;
  Selection? afterSelection;
}
Never modify the document directly. Always use transactions to ensure proper state management, undo/redo support, and collaborative editing compatibility.

Creating Transactions

Get a transaction from EditorState:
final transaction = editorState.transaction;
// Automatically sets beforeSelection to current selection
Or create manually:
final transaction = Transaction(document: document);
transaction.beforeSelection = editorState.selection;

Operation Types

Transactions contain operations that represent changes to the document.

InsertOperation

Inserts nodes at a specific path:
class InsertOperation extends Operation {
  final Path path;
  final Iterable<Node> nodes;
}

DeleteOperation

Deletes nodes at a specific path:
class DeleteOperation extends Operation {
  final Path path;
  final Iterable<Node> nodes;  // Nodes being deleted (for undo)
}

UpdateOperation

Updates node attributes:
class UpdateOperation extends Operation {
  final Path path;
  final Attributes attributes;      // New attributes
  final Attributes oldAttributes;   // Previous attributes (for undo)
}

UpdateTextOperation

Updates text content using deltas:
class UpdateTextOperation extends Operation {
  final Path path;
  final Delta delta;     // Changes to apply
  final Delta inverted;  // Inverse changes (for undo)
}

Node Operations

Insert Nodes

final transaction = editorState.transaction;

// Insert single node
transaction.insertNode(
  [0],  // Insert at path [0]
  Node(
    type: 'paragraph',
    attributes: {'delta': [{'insert': 'Hello'}]},
  ),
);

// Insert multiple nodes
transaction.insertNodes(
  [1],
  [
    paragraphNode(text: 'First'),
    paragraphNode(text: 'Second'),
  ],
);

await editorState.apply(transaction);

Delete Nodes

final transaction = editorState.transaction;

// Delete single node
final node = editorState.getNodeAtPath([0]);
if (node != null) {
  transaction.deleteNode(node);
}

// Delete multiple nodes
transaction.deleteNodes([node1, node2]);

// Delete by path
transaction.deleteNodesAtPath([0], 2);  // Delete 2 nodes starting at [0]

await editorState.apply(transaction);

Update Node Attributes

final transaction = editorState.transaction;
final node = editorState.getNodeAtPath([0]);

if (node != null) {
  transaction.updateNode(node, {
    'align': 'center',
    'backgroundColor': '0xFFFFFF00',
  });
}

await editorState.apply(transaction);

Move Nodes

final transaction = editorState.transaction;
final node = editorState.getNodeAtPath([0]);

if (node != null) {
  transaction.moveNode([3], node);  // Move to path [3]
}

await editorState.apply(transaction);

Text Operations

The TextTransaction extension provides convenient methods for text manipulation.

Insert Text

final transaction = editorState.transaction;
final node = editorState.getNodeAtPath([0]);

if (node != null) {
  transaction.insertText(
    node,
    5,  // Insert at offset 5
    'Hello',
    attributes: {'bold': true},
  );
}

await editorState.apply(transaction);

Parameters

  • node: The node to insert text into
  • index: Character offset for insertion
  • text: Text to insert
  • attributes: Optional formatting attributes
  • toggledAttributes: Attributes from toggled formatting
  • sliceAttributes: Whether to inherit surrounding attributes (default: true)

Delete Text

transaction.deleteText(
  node,
  0,   // Start offset
  5,   // Length to delete
);

Format Text

Apply formatting to a text range:
transaction.formatText(
  node,
  0,   // Start offset
  10,  // Length
  {'bold': true, 'italic': true},
);

Replace Text

transaction.replaceText(
  node,
  0,      // Start offset
  5,      // Length to replace
  'Hi',   // Replacement text
  attributes: {'bold': true},
);

Merge Text

Merge two text nodes:
final leftNode = editorState.getNodeAtPath([0]);
final rightNode = editorState.getNodeAtPath([1]);

if (leftNode != null && rightNode != null) {
  transaction.mergeText(
    leftNode,
    rightNode,
    leftOffset: leftNode.delta?.length,  // Merge at end of left
    rightOffset: 0,  // Start of right
  );
}

Insert Delta

Insert a complete delta:
final insertDelta = Delta()
  ..insert('Hello', attributes: {'bold': true})
  ..insert(' world');

transaction.insertTextDelta(
  node,
  0,  // Insert at offset 0
  insertDelta,
);

Applying Transactions

Basic Application

final transaction = editorState.transaction;
// ... add operations ...
await editorState.apply(transaction);

With Options

await editorState.apply(
  transaction,
  options: const ApplyOptions(
    recordUndo: true,   // Add to undo stack
    recordRedo: false,  // Don't add to redo stack
  ),
);

Without Selection Update

await editorState.apply(
  transaction,
  withUpdateSelection: false,
);

Skip History Debounce

await editorState.apply(
  transaction,
  skipHistoryDebounce: true,  // Immediately seal history item
);

Selection Management

Transactions automatically manage selection:
final transaction = editorState.transaction;
// beforeSelection is set to current selection automatically

transaction.insertText(node, 0, 'Hello');
// afterSelection is automatically set to Position after insertion

await editorState.apply(transaction);
// Selection is updated to afterSelection

Manual Selection Control

transaction.beforeSelection = customBeforeSelection;
transaction.afterSelection = customAfterSelection;

Selection Update Reason

transaction.reason = SelectionUpdateReason.transaction;

Custom Selection Type

transaction.customSelectionType = SelectionType.block;

Operation Composition

Transactions automatically compose operations for efficiency:
final transaction = editorState.transaction;

// These operations on the same node are composed
transaction.insertText(node, 0, 'Hello');
transaction.insertText(node, 5, ' world');
// Results in a single UpdateTextOperation

Delta Composition

Text operations are composed automatically:
final transaction = editorState.transaction;

transaction.insertText(node, 0, 'A');
transaction.insertText(node, 1, 'B');
transaction.insertText(node, 2, 'C');
// All composed into one UpdateTextOperation

await editorState.apply(transaction);

Transaction Transformation

Operations are transformed to handle path changes:
final transaction = editorState.transaction;

// Insert at [0]
transaction.insertNode([0], newNode);

// This operation's path is automatically transformed to [1]
transaction.deleteNodesAtPath([0]);  
// Actually deletes what was originally at [0], now at [1]

Undo/Redo Integration

Transactions integrate seamlessly with the undo/redo system.

Recording Undo

// Default: record in undo stack
await editorState.apply(
  transaction,
  options: const ApplyOptions(recordUndo: true),
);

Recording Redo

// Used internally for redo operations
await editorState.apply(
  transaction,
  options: const ApplyOptions(recordRedo: true),
);

In-Memory Updates

// Don't record in history (e.g., for temporary updates)
await editorState.apply(
  transaction,
  options: const ApplyOptions(
    recordUndo: false,
    inMemoryUpdate: true,
  ),
);

Remote Transactions

For collaborative editing, apply remote transactions:
await editorState.apply(
  transaction,
  isRemote: true,  // Indicates this came from another user
);
Remote transactions:
  • Don’t trigger recordUndo
  • Transform local selection based on operations
  • Set SelectionUpdateReason.remote

Transaction Events

Listen to transaction events:
editorState.transactionStream.listen((value) {
  final (time, transaction, options) = value;
  
  if (time == TransactionTime.before) {
    // Transaction about to be applied
    print('Will apply ${transaction.operations.length} operations');
  } else {
    // Transaction has been applied
    print('Applied ${transaction.operations.length} operations');
    
    // Send to backend for collaborative editing
    sendToBackend(transaction.toJson());
  }
});

JSON Serialization

Transactions can be serialized for storage or network transmission:
// To JSON
final json = transaction.toJson();
/*
{
  'operations': [
    {'op': 'insert', 'path': [0], 'nodes': [...]},
    {'op': 'update_text', 'path': [1], 'delta': [...], 'inverted': [...]}
  ],
  'before_selection': {'start': {...}, 'end': {...}},
  'after_selection': {'start': {...}, 'end': {...}}
}
*/

Practical Examples

Insert Paragraph with Text

final transaction = editorState.transaction;

transaction.insertNode(
  [0],
  paragraphNode(
    delta: Delta()..insert('Hello, world!'),
  ),
);

transaction.afterSelection = Selection.single(
  path: [0],
  startOffset: 13,
);

await editorState.apply(transaction);

Bold Selected Text

final selection = editorState.selection;
if (selection != null && !selection.isCollapsed) {
  final transaction = editorState.transaction;
  final nodes = editorState.getNodesInSelection(selection);
  
  for (final node in nodes) {
    if (node.delta != null) {
      transaction.formatText(
        node,
        selection.startIndex,
        selection.length,
        {'bold': true},
      );
    }
  }
  
  await editorState.apply(transaction);
}

Delete Selected Content

final selection = editorState.selection;
if (selection != null && !selection.isCollapsed) {
  final transaction = editorState.transaction;
  final nodes = editorState.getNodesInSelection(selection);
  
  if (selection.isSingle) {
    // Single node: delete text
    final node = nodes.first;
    transaction.deleteText(
      node,
      selection.startIndex,
      selection.length,
    );
  } else {
    // Multiple nodes: delete nodes
    transaction.deleteNodes(nodes);
  }
  
  await editorState.apply(transaction);
}

Convert Paragraph to Heading

final node = editorState.getNodeAtPath([0]);
if (node != null && node.type == 'paragraph') {
  final transaction = editorState.transaction;
  
  // Create new heading node
  final heading = node.copyWith(
    type: 'heading',
    attributes: {
      ...node.attributes,
      'level': 1,
    },
  );
  
  transaction.deleteNode(node);
  transaction.insertNode([0], heading);
  
  await editorState.apply(transaction);
}

Batch Operations

final transaction = editorState.transaction;

// Multiple operations in one transaction
transaction.insertNode([0], paragraphNode(text: 'First'));
transaction.insertNode([1], paragraphNode(text: 'Second'));
transaction.insertNode([2], paragraphNode(text: 'Third'));

// All applied atomically
await editorState.apply(transaction);

Best Practices

One Transaction Per User Action

Group related operations into a single transaction for proper undo/redo.

Set afterSelection

Always set meaningful afterSelection for better UX.

Use Text Helpers

Use insertText, deleteText, etc. instead of manual delta operations.

Handle Edge Cases

Check for null nodes and validate paths before operations.

See Also

Build docs developers (and LLMs) love