Block Model

For each block, BlockSuite creates a block model to simplify its manipulation.

Block

The Block interface serves as the data abstraction for a block. It includes three key properties:

  1. **flavour**: Indicates the type of the block, such as paragraph, code, or list.

  2. **id**: A unique identifier for the block. This ID can be used to query, update, or delete the block.

  3. **model**: Represents the block's model.

Model

A block model provides an interface for reading and writing data within a block. Each piece of data in a block is referred to as a prop, which can be accessed through model.props.

For example:

const blockA = store.getBlock('blockAId');
const blockAModel = blockA.model;

// Retrieve a prop value
const height = blockAModel.props.height;

// Update a prop value
store.updateBlock(blockAModel, () => {
  blockAModel.props.height = height * 2;
})

Developers do not need in-depth knowledge of Yjs or its API. The block model serves as a bridge between Yjs and native JavaScript data, simplifying data manipulation within blocks.

For more information on how the block model works with Yjs, please refer to Block Reactive.

Block CRUD

You may have noticed in the previous example that we can perform CRUD operations on blocks using the block model:

// Create a block
const blockAId = store.addBlock('affine:paragraph', {
  type: 'h1',
  text: new Text('Hello, blocksuite')
});

// Query a block
const blockA = store.getBlock(blockAId);

// Update a block
store.updateBlock(blockA.model, () => {
  blockA.model.type = 'h2';
});

// Delete a block
store.deleteBlock(blockA.model);

This approach allows you to easily create, read, update, and delete blocks within the store.

Block Schema

A block schema defines the structure and properties of a block. Below, I'll guide you through the process of defining a block schema step by step.

1. Define the Block Props

Each block has a set of props that determine how its data is displayed. For example, a typical paragraph block might have the following props:

import type { Text } from '@blocksuite/store';

export type ParagraphProps = {
  type: 'text' | 'h1' | 'h2' | 'h3';
  text: Text;
};

2. Define the Block Model

Every block requires a model. You can create a block model by extending the BlockModel class:

import { BlockModel } from '@blocksuite/store';

export class ParagraphBlockModel extends BlockModel<ParagraphProps> {
  override isEmpty(): boolean {
    return this.props.text$.value.length === 0 && this.children.length === 0;
  }
  // Additional data layer methods and helpers can be added here
}

3. Define the Schema

Next, use defineBlockSchema to define the schema for your block:

import { defineBlockSchema } from '@blocksuite/store';

export const ParagraphBlockSchema = defineBlockSchema({
  flavour: 'affine:paragraph',
  props: (internal): ParagraphProps => ({
    type: 'text',
    text: internal.Text(),
  }),
  metadata: {
    version: 1,
    role: 'content',
    parent: [
      'affine:note',
      'affine:paragraph',
    ],
  },
  toModel: () => new ParagraphBlockModel(),
});

In the metadata property, we specify the valid parent blocks for this block. You can also define valid child blocks by declaring the children property.

4. Convert Schema into an Extension

Finally, convert the schema into an extension:

import { BlockSchemaExtension } from '@blocksuite/store';

export const ParagraphBlockSchemaExtension =
  BlockSchemaExtension(ParagraphBlockSchema);

5. Use the Extension

To use this extension, simply add it to the list of extensions when initializing the Store:

new Store({
  // ...
  extensions: [
    // ...other extensions
    ParagraphBlockSchemaExtension
  ]
});

By following these steps, you can successfully define and use a custom block schema in your application.

Optional Property

In a CRDT-based editor, data migration is considered impossible because there is no central server to serve as a "single source of truth." Therefore, if you want to add new properties to an existing block, it is best to declare them as optional.

Fortunately, defining an optional property in BlockSuite is very straightforward. You can simply set its default value to undefined:

export type ParagraphProps = {
  type: 'text' | 'h1' | 'h2' | 'h3';
  text: Text;
  newProperty?: number;
};

export const ParagraphBlockSchema = defineBlockSchema({
  // ...
  props: (internal): ParagraphProps => ({
    type: 'text',
    text: internal.Text(),
    newProperty: undefined,
  }),
});