Skip to content

feat: API design feedback #1756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions adr/2025_06_13-document-transforms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Document Transforms

A core part of the BlockNote API is the ability to make changes to the document, using BlockNote's core design of blocks, this is much easier to do programmatically than with other editors.

We've done pretty well with our existing API, but there are a few things that could be improved.

Referencing content within the document is either awkward or non-existent. Right now we essentially really only have an API for referencing blocks by their id with no further level of granularity.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, I think the concept of a Location is useful to have - but if we start implementing this I think we should have more sight on practical use-cases. i.e. which use-cases / APIs do we want to "unlock" with this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

insertContentAt(at: Location, content: Block | PartialBlock | string)
getSelection(): {range: Range}

A number of the low-level Tiptap API uses for things like deleting some chars

This will also be very useful for server-side editing ops like rewriting a paragraph


## Locations

[Looking at Slate](https://docs.slatejs.org/concepts/03-locations) (highly recommend reading the docs), they have the concept of a `Location` which is a way to reference a specific point in the document, but it does not have to be so specific as positions, it has levels of granularity.

This gives us a unified way to reference content within the document, allowing for much more granular editing. Take a look at the `Location.ts` file for more details around this.
Copy link
Collaborator

@matthewlipski matthewlipski Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to necessarily use the Location concept? I like the granularity but it feels like we could achieve something similar by just using block/inline content ID references. I.e.:

  • Low granularity: Point to the start/end of a block by its ID.
  • Mid granularity: Point to the start/end of inline content by its ID.
  • High granularity: Point to the start of inline content with an offset by its ID.

I feel like Slate's Location is necessary due to the fact that you can only reference nodes by their positions. Since we set IDs, I think we should make use of that since imo it'll result in a simpler API. So our Location could look more like:

type Location = {
  id: string;
  side?: "start" | "end";
  offset?: number
}

Copy link
Contributor Author

@nperez0111 nperez0111 Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline content by its ID.

Inline content doesn't have an ID does it? And, I don't think it should, it'd be too granular and require wrapping things in marks everywhere.

I'm willing to remove Path as pointed out here: #1756 (comment), and, I've now updated the typescript file to represent that.

I don't think your proposal for a Location captures everything that Slate's could though, such as representing a Range which is extremely useful.

Using your language, I think we can achieve this with:

/**
 * A block id is a unique identifier for a block, it is a string.
 */
export type BlockId = string;
/**
 * A block identifier is a unique identifier for a block, it is either a string, or can be object with an id property (out of convenience).
 */
export type BlockIdentifier = { id: BlockId } | BlockId;
/**
 * A point has an id and an offset, it is used to identify a specific position within a block.
 */
export type Point = {
  id: BlockId;
  offset: number;
};
/**
 * A range is a pair of points, it is used to identify a range of blocks within a document.
 */
export type Range = {
  anchor: Point;
  head: Point;
};
/**
 * A location is an id, point, or range, it is used to identify positions within a document.
 */
export type Location = BlockId | Point | Range;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point about ranges, missed that. I'm wondering if there's a use case for pointing to a specific inline content that we would miss by just having block + offset, maybe for extensions which work with links?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To point to a specific inline content, you could have a Point which points at the start of the inline content, or a Range that encapsulates the whole inline content range:

{id: 'abc', content:[{type:'link', text: 'ABC'}]}

const point = { id: 'abc', offset: 0 };

const range: { anchor: { id: 'abc', offset: 0 }, head: { id: 'abc', offset: 3 } }


## Transforms separation of concerns

Right now all transformation functions are defined directly on the `Editor` instance, this is not ideal because it only further muddles the API.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. I think maybe this should be a separate work-item ("cleaning up BlockNoteEditor"). Doing this hand-in-hand with updating documentation probably gives us a lot of insights into what we think would be a better organization of functions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working on that 😉


Instead, we should have a separate `Transform` class which defines methods that operate on the editor's document to make changes to it. This will also be very useful for doing server-side transformations.

```ts
editor.transform.insertBlocks(ctx:{
at: Location,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it useful to insertblocks at a Location vs the current API (before / after / inside an existing block)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Toss-up for insertBlocks, but for insertContent(ctx: {at: Location; content: string })

blocks: Block | Block[]
});

editor.transform.replaceBlocks(ctx: {
at: Location,
with: Block | Block[]
})

editor.transform.replaceContent(ctx: {
at: Location,
with: InlineContent | InlineContent[]
})

editor.transform.deleteContent(ctx: {
at: Location,
})
```

## References
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, currently things like comments can't be represented in the BlockNote API, and indeed, this would enable us to separate them from the document data definitely feels like the "right approach"

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I like this idea a lot, feels more general than ProseMirror's decorations. In theory it would also make it much nicer for our UI architecture, since instead of writing plugins you could just use references. E.g. for the formatting toolbar, you could listen to selection changes and create a reference pointing to the selection when you want to show it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g. for the formatting toolbar, you could listen to selection changes and create a reference pointing to the selection when you want to show it.

🤯 what a great idea! If it were robust enough for that, this could simplify a ton of things!


Currently, we do not have a good way to reference things about content within a document, except by block id. With Locations, we can be much more granular & more powerful.

I think one application of this would be to introduce the concept of "references" to content within a document. For example, we currently do not store comments into the blocknote json, taking the position that it really is not part of the document, but rather metadata about the document.

With references, we could store comments with references to the blocks that they are about. And map those references through changes if they are ever modified. For example, if someone commented on the text within block id `123`, and then the block was moved to a new location, we could update the comment to reference the new block id.

So, this would allow a comment to still be a separate entity, but be able to "hydrate" within the editor and keep track of the content that it was about.

```ts
type Reference<Metadata extends Record<string, any>> = {
to: Location,
metadata: Metadata
}

type Comment = Reference<{
threadId: string
}>

editor.references.add(reference: Reference, onUpdate: (reference: Reference) => void);
```

This is inspired by [bluesky's concept of facets for rich text content](https://docs.bsky.app/docs/advanced-guides/post-richtext) which is a great example of how to make block note content more inter-operable between different applications.
60 changes: 60 additions & 0 deletions adr/2025_06_13-extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# BlockNote Extension API

It is definitely more designed by accident than intention, so let's put some thought into the sort of API we would want people to build extensions with.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before diving into technical details below, let's zoom back out and ask ourselves what an Extension actually is / should be used for.

When should people use extensions? And what for? Why do we want to introduce the concept of an extension?

Having some clarity to these questions will help us to both better explain the concept to users, and help us create a better API design

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An extension is a way to extend the editor with additional functionality, in a way that is integrated with the editor API.

Examples of when to use an extension:

You want to add behaviors to the editor that are reactive to the editor's current state such as:

  • menus and toolbars (e.g. cmd-k for AI ops)
  • keyboard shortcuts (e.g. adding a shortcut for inserting a custom block)
  • side-effects (e.g. collaborative editing with another lib)

These extensions have APIs of their own, which should be accessible across the UI, (e.g. a button may open a menu).

We want to introduce the concept of an extension for 2 reasons:

  • We already are using an extension system internally, prosemirror plugins, which are difficult to reason about because of the level of abstraction they operate at.
  • We can expose an API for users to extend the functionality of the editor at a higher-level API, which is then even sharable across projects (e.g. we may not want to introduce a step custom node, but it'd be nice for the community to be able to add one, or open project tasks, etc.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on our discussion here, I think I've come around to something. I think that there are 2 separate needs here like you pointed out:

  • There is a need for a higher-level API than what prosemirror affords. This would be for adding low-level plugins to the editor. Examples would include things like menus, toolbars, and things that need access to editor state like table of contents.

  • There is also a need for a higher-level packaging that "bundles" editor functionality into a single distributable packaging. It may include things like adding blocks to the schema, adding lower-level plugins for exposing new methods to the editor (e.g. editor.extensions.ai.acceptChanges())

I'll propose the naming of: extension (low-level) vs. bundle (high-level)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool. I like the separation, I think when we work on this it will become clear if the separation will be needed and / or whether we need to expose both to consumers


## Core Requirements

What I identified was:

- The ability to "eject" to the prosemirror API, if we don't provide something good enough. This is a core choice, and one I don't think we would ever need to walk back on. Fundamentally, we do not want to expose absolutely everything that Prosemirror can do, but also do not want to stop those with the know-how to actually get stuff done.
- A unified store API, to make consuming extension state homogenous, this will be discussed further below
- Life-cycle event handlers, by providing hooks for `mount` and `unmount` we allow extensions to attach event handlers to DOM elements, and have a better general understanding of the current state of the editor.

## State Management

What I had the most trepidation about was deciding on whether we should prescribe a state management library, and if so, which one.

I think the answer is _yes_, we should prescribe a state management library, and I think the answer is @tanstack/store.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the best way to validate this is to create a PR that migrates the AI extension from zustand to tanstack. Let's create a task for this?


<details>
<summary>Why @tanstack/store? And not something else?</summary>
It comes with a few benefits:

- It gives a single API for all state management, which is more convenient/consistent for consumers

As for which library to use, I think we should use @tanstack/store.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate


- The store is very simple, and can easily be re-implemented if needed
- There is already bindings for most major frameworks, but can be used without them
- It seems that anything tanstack does will be widely adopted so it should be a pretty safe bet

What I had trouble with is there are a few different use-cases for state management, events like we have now aren't great because they put the burden on the consumer to manage the state. Or, they can emit an event (e.g. `update`), but then have to round-trip back to the extension to get the state (and somehow store it on their side again).

Something like observables have a nicer API for this, but they are for pushing data (i.e. multiple readers), not for pulling it (i.e. any writers). They also have the same problem of putting the burden on the consumer.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have an example of what you mean with but they are for pushing data (i.e. multiple readers), not for pulling it (i.e. any writers)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An Observable is a one-way stream, like an event emitter.

From RXJS Observable:
image

So, they still don't fix the current problem we have with events because they are still putting the burden on consumers.

TanStack & Zustand are "push-pull" systems where you get a snapshot (i.e. a pull), and also be notified of changes (i.e. a push).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah got it, coming from RX I think you're right. I was thinking about MobX observables which (if I'm not mistaken) are more like (deep) signals

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, overloaded term, mobx is based on signals since it is "push-pull"


Signals are a nice middle ground, being that they are for both pushing & pulling data. The problem is that there are many implementations, and not super well-known in the React ecosystem.

Zustand is a popular library, but allowing partial states makes it somewhat unsafe in TypeScript.

Jotai is probably my second choice, but it makes it a bit awkward to update states because it relies on a separate store instance rather than the "atom" being able to update itself <https://jotai.org/docs/guides/using-store-outside-react>.
</details>

## Exposing extension methods
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll need to expose them in a type-safe way imo. What do you think of the pattern with getAIExtension atm?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was leaning more towards something like editor.extension.ai since the editor can know what extensions it has been given.

Though this can get awkward if you don't have the consumer's editor instance. So we likely would have a helper similar to that. But, I wanna see how that shakes out type-wise

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt this will work type-wise, but curious what you come up with!


Not everything can be communicated through just state, so we need to be able to expose additional methods to the application.

I propose that we have a `extensions` property on the `BlockNoteEditor` instance, which is a map of the extension key to the extension instance, but filtered out to only include non-blocknote methods (as defined by the `ExtensionMethods` type).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious to see what you have in mind for how extensions should be defined too. Right now, to make something resembling an extension using the BlockNote API only, you generally listen to editor.onChange/useEditorChange and go from there, whether it's making changes to the editor, or doing stuff outside the editor.

So I'm wondering if all we really need an extension to be is a state + listeners, sort of like this:

function defineExtension<State extends Record<string, any>>({
  initialState: State;
  listeners?: Partial<{
    onCreate: (editor: BlockNoteEditor) => State,
    onContentChange: (editor: BlockNoteEditor) => State,
    onSelectionChange: (editor: BlockNoteEditor) => State,
    onDestroy: (editor: BlockNoteEditor) => State,
  }>
}) {...}

Copy link
Contributor Author

@nperez0111 nperez0111 Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Described in the TS file here:

export interface BlockNoteExtensionConfig<State> {

I also thought it'd be good to have hooks for these sort of event's, but Yousef pointed out they were already available on the editor instance.


This will allow the application to access the extension methods, and also allows us to keep the extension instance private to the editor (type-wise).

# BlockNote Bundles

Somewhat related, to extensions, we also need a way for bundling sets of editor elements in a single packaging. This will be a much higher-level API, which will aim to provide a single import for adding an entire set of functionality to the editor (e.g. adding multi-column support, or comments, etc.)

## Core Requirements

- A way to add blocks, inline content, and styles to the editor
- A way to add extensions to the editor
- A way to add to the dictionary of locales to the editor
- A way to add to the slash menu
- A way to add to the formatting toolbar
36 changes: 36 additions & 0 deletions adr/2025_06_13-packages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# BlockNote Packages & Sub-Packages
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. What's your reasoning for leaning heavy into subpackages? I don't think this is very common, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaning heavy into sub-packages isn't super common for editors, because they normally split on the package level, and most editors are fairly old (i.e. pre-subpackages).

I've always hated how many packages there are just to get an editor going. But, the main trade-off of splitting at the package level is that it makes the install size smaller and feels "composable", users can get into weird states (e.g. a user using a new version that depends on a core feature that didn't exist on their version). This sort of a approach, completely side-steps package version drift, while also not making the top-level API just a huge list of options. It also is nice for code-splitting and general "organization", we can treat sub-packages as a proper top-level package and design it's API to be more sensible than a huge export of just everything we have.

Copy link
Collaborator

@YousefED YousefED Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not making the top-level API just a huge list of options

Cool, seems like this is the main reason. Note to self to summarize:

  • packages come with a lot of bundling / dependency / organizational overhead
  • just using a strict folder structure (which imo would be the other alternative) doesn't achieve the goal of cleanly organizing the API (unless we export using namespaces)

Also agree with the second point? I.e.: better to use subpackages vs namespaces? One benefit of namespaces I suppose would be that it's more discoverable (e.g.: when you do import * you easily get to see everything that's available)

(note that with namespaces I mean that for example, instead of exporting all blocks top-level, we can export a "blocks" object which encapsulates the default blocks. i.e. import { blocks } from "@blocknote/core". In your design this would be in subpackage @blocknote/core/blocks)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think a strict folder structure with namespaces is conceptually very similar.
Also sub-packages are more fine-grained for package resolution, you don't need to tree-shake what was never imported.


## Core

The `@blocknote/core` package should contain everything needed to build a pure JS editor instance. It has no opinion on the UI layer, and is only concerned with the core editor functionality.

It contains sub-packages for:

- `@blocknote/core/blocks` - Contains the default blocks, inline content, styles and their implementations (excluding things like blockContainer, blockGroup, etc which are core to the editor but should not be part of the schema).
- `@blocknote/core/blocks/header` - Contains the header block.
- `@blocknote/core/blocks/code-block` - Contains the code block block.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

except the code-block will probably stay in a different package right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is the code-block default block, and there is the code-block package which would be for things like the shiki languages and all that. Probably worthwhile to revisit the naming there.

- etc...
- `@blocknote/core/extensions` - Re-exports the extension packages below.
- `@blocknote/core/extensions/comments` - Contains the comments extension.
- `@blocknote/core/exporter` - Contains the exporter package.
- `@blocknote/core/exporter/html` - Contains the html exporter package.
- `@blocknote/core/exporter/markdown` - Contains the markdown exporter package.
- `@blocknote/core/server-util` - Contains the server util package.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if server-util should be part of core as it might pull in some other deps?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worthwhile to look into, I should mention that the main reason to split to another package is around deps & install size (or being a very different concern of course)

- `@blocknote/core/locales` - Contains the locales package.

## React

The `@blocknote/react` package should contain everything needed to build a React editor instance. It has no opinion on the core editor functionality, and is only concerned with the React UI layer.

It contains sub-packages for:

- `@blocknote/react/editor` - Contains the editor package.
- `@blocknote/react/comments` - Contains the comments package.
- `@blocknote/react/table-handles` - Contains the table handles package.
- etc... (need to figure out what needs to be exported vs. available and how coupled this all is)

## Editor instance

The editor instance has been growing in complexity, and handles too many different concerns. To focus it, we should split it up into something like: the `BlockNoteEditor.ts` file.

The goal would be to group the functionality of the editor instance in such a way that it is easier to navigate and understand.
21 changes: 21 additions & 0 deletions adr/2025_06_13-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# BlockNote Schema

Right now it is overly burdensome to have to pass around 3 different types to the editor, and it is also not very type-safe (when you just end up with `any` everywhere).

The idea is to have a single type that is a union of the 3 types, and then make type predicates available to check if accessed properties are valid (and likely just assertions too).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be great if we can get this to work!


You'll see some of what I came up with in the `Schema.ts` file.

You'll also notice that the default blocks, inline content, styles, and groups are all defined in the `@blocknote/core/blocks` package. This is assuming that we have already moved them to the blocknote API and out of the core package.

## Groups

In a somewhat similar vein, I think there might be use for having an indirection layer for referring to specific blocks, inline content, styles, etc. Reason being that it allows callers to refer to a group of things with a single identifier, and allow customizing that membership by just modifying that group.

Examples include:

- Keyboard shortcuts can refer to a group of blocks/inline-content/styles without modification to the handler
- Relationships between blocks/inline-content/styles can be defined (e.g. allow for a todo block to only have todo item children)
- Properties of blocks/inline-content/styles can be defined (e.g. adding `heading` to the `toggleable` group)

This may or may not be useful, but it is a thought.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, let's keep in mind 👍

28 changes: 28 additions & 0 deletions adr/2025_06_16-configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# BlockNote Configuration

Editors are widely different across applications, often with users having opposing requirements. Most editors explode with complexity when trying to support all of these use cases. So, there needs to be a guide for the different ways to configure so that there is not just a single overwhelming list of options.

Fundamentally, there are few different kinds of things to be configured:

- **block-configuration**: The configuration for a specific kind of block
- e.g. the `table` block might have toggles for enabling/disabling the header rows/columns
- **schema-configuration**: The configuration of what blocks, inline content, and styles are available in the editor
- e.g. whether to include the `table` block at all
- **extension-level-configuration**: The configuration of the extension itself
- e.g. what ydoc should the collaboration extension use
- **extension-configuration**: The configuration of the extensions that are available in the editor
- e.g. whether to add collaboration to the editor
- **editor-view-configuration**: The configuration of the editor views
- e.g. whether to show the sidebar, the toolbar, etc.
- **editor-configuration**: The configuration of the editor itself
- e.g. how to handle paste events

These forms of configuration are not mutually exclusive, and can be combined in different ways. For example, knowing that the editor has collaboration enabled, might change the what the keybindings do for undo/redo.

In an ideal world, these configurations would be made at the "lowest possible level", like configuring the number of levels of headings would be configured when composing the schema for the editor, rather than at the editor level.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great principle 🙌


Configuration should be publicly accessible, so that mixed combinations can be created (i.e. different behaviors for an editor with or without collaboration).

## TODO

- Describe how you configure at the block level, then compose that into a schema, the compose that into an editor, and then compose that into a view.
155 changes: 155 additions & 0 deletions adr/2025_06_16-nested-blocks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# BlockNote Nested Blocks

There are two separate problems when it comes to "nested blocks" support in BlockNote:

- **nested-blocks** The first is the ability for a block to contain other blocks within it (e.g. a table cell having not just inline content, but actual other blocks inside it)
- **content-fields** The second is the ability for a block to contain multiple pieces of content within it (e.g. an alert block having a title & description field (which contain inline content))

Let's start with the first problem, nested blocks. By describing existing block relationships:

## Block with inline content

This is the simplest case, and the only one that is currently supported by BlockNote.

```json
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth mentioning that the current inline content object shape is a mess. You have StyledText which is a different shape to Link, which is also different to custom inline content. This makes trying to do basic things like reading plain text kind of a nightmare, and is something that we definitely need to address.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thing to add, I don't have an answer for this atm. But feel free to write a proposal

{
"type": "block-type",
"content": [
{
"type": "text",
"text": "Hello, world!"
}
],
}
```

There is a 1:1 relationship between the block type and it's content. And, no restrictions of the inline content allowed within the block.

> TODO come up with a description of the sorts of keybinds & cursor behavior that should be supported for this block type

## Block with nested blocks

This is a proposal for how to support nested blocks.

```json
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is technically what we already support to some level, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, modeled on the multi-column, but generic for others to implement

"type": "custom-block-type",
"props": {
"abc": 123
},
"content": null,
"children": [
{
"type": "nested-block",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nested-block is analagous to the current blockGroup right?

"content": [
{
"type": "block",
"content": [
{
"type": "text",
"text": "Hello, world!"
}
]
}
]
}
]
}
```

This would completely enclose all nested blocks within the `custom-block-type` block. And, works the same way as the multi-column example.

> TODO come up with a description of the sorts of keybinds & cursor behavior that should be supported for this block type

## Block with structured inline content fields

This is a proposal for how to support multiple inline content fields within a block.

```json
{
"type": "alert",
"content": null,
"children": [
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this will work out. now suddenly, children can not only contain blocks anymore, but also inline content. I'm afraid this will break quite a bit (both technically and in terms of explainability)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or wait, alert-title and alert-content are considered blocks here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, considered blocks. Would need to introduce the concept of a "nested block" which is not valid on it's own but only in the context of being part of another block.

This is also shown with the table example below

"type": "alert-title",
"content": [
{
"type": "text",
"text": "Hello, world!"
}
]
},
{
"type": "alert-content",
"content": [
{
"type": "text",
"text": "Hello, world!"
}
]
}
]
}
```

The core idea is that the `parent` block restricts what content is allowed within it's children.

> TODO come up with a description of the sorts of keybinds & cursor behavior that should be supported for this block type

## Examples

### Rebuilding tables with nested blocks

Using this new structure, we can rebuild tables if we had this new API:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense, but 2 questions:

  • The consumer will have to specify when creating a block, which blocks it can be a part of or which blocks it can contain, right? And for the alert example, there will have to be some way of enforcing that each alert block must have 1 alert title followed by 1 alert description. I assume we would use something like the ProseMirror group and content fields for this?
  • If you wanted to have nested blocks in the table block, would you just append nested-block at the end of the children? If so, is it possible to have e.g. children: [{ type: "block1", ... }, { type: "nested-block", ... }, { type: "block2", ... }]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The consumer will have to specify when creating a block, which blocks it can be a part of or which blocks it can contain, right? And for the alert example, there will have to be some way of enforcing that each alert block must have 1 alert title followed by 1 alert description. I assume we would use something like the ProseMirror group and content fields for this?

Correct. We'd have to have prosemirror enforce this, we can decide how best to let the consumer describe this structure. I proposed groups partially for this reason:

## Groups
In a somewhat similar vein, I think there might be use for having an indirection layer for referring to specific blocks, inline content, styles, etc. Reason being that it allows callers to refer to a group of things with a single identifier, and allow customizing that membership by just modifying that group.
Examples include:
- Keyboard shortcuts can refer to a group of blocks/inline-content/styles without modification to the handler
- Relationships between blocks/inline-content/styles can be defined (e.g. allow for a todo block to only have todo item children)
- Properties of blocks/inline-content/styles can be defined (e.g. adding `heading` to the `toggleable` group)
This may or may not be useful, but it is a thought.

If you wanted to have nested blocks in the table block, would you just append nested-block at the end of the children?

Tables would be defined to only allow table-cells as children, so no other blocks would be allowed within a table block. However, table-cells would be allowed to have arbitrary blocks (or, not if they were configured not to).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, so this is more of a change of concept of what a nested blocks actually is I suppose. Because right now, it's an inherent property of all blocks, that they can have nested blocks that are indented below. Whereas in this new design, nested blocks are a lot more flexible, and the indented nested blocks are just a generic implementation.

Am I understanding that right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to define my terms here:

- **nested-blocks** The first is the ability for a block to contain other blocks within it (e.g. a table cell having not just inline content, but actual other blocks inside it)
- **content-fields** The second is the ability for a block to contain multiple pieces of content within it (e.g. an alert block having a title & description field (which contain inline content))

But, I see that "nested-blocks" is indeed overloaded, take it to mean "blocks as content", maybe.


```json
{
"type": "table",
"content": null,
"props": {
"backgroundColor": "default",
"textColor": "default",
"columnWidths": [100, 100, 100],
"headerRows": 1,
"headerCols": 1,
},
"children": [
{
"type": "table-row",
"content": null,
"children": [
{
"type": "table-cell",
"content": null,
"children": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Hello, world!"
}
]
}
]
},
{
"type": "table-cell",
"content": null,
"children": [
{
"type": "paragraph",
"content": [
{
"type": "text",
"text": "Hello, world!"
}
]
}
]
}
]
}
]
}
```
Loading
Loading