Skip to content
FrameworkJavaScript

The Blok class: create and destroy an editor

The main editor class. It creates the Blok editor instance and manages it. Every namespace a tool reaches through api.* is also on the instance as editor.*. The properties below are that same surface, plus the width, placeholder, tokens and i18n namespaces the class declares itself.

Last updated Oct 7, 2026Edit this page on GitHub

Reaching the editor instance

The methods below run on the editor you created with new Blok(). They are available once editor.isReady resolves.

TypeScript
// You already hold the instance returned by the constructor.
const editor = new Blok({ holder: 'editor' });
await editor.isReady;

// Call any API method on it.
editor.caret.setToLastBlock('end');

Methods

save()

Promise<OutputData>

Extracts the current editor content as structured JSON data. This is the main method for saving editor content.

Rich text fields, such as paragraph.text, hold segments: arrays of { text, marks? } runs. A document saved by an older version can hold HTML strings until it is saved again. migrateToRichText converts it.

When to use

Call it after await editor.isReady. The returned JSON is your source of truth. Store it and pass it back to render().

Errors

  • The editor is in read-only mode when save() is called.

    Blok's content can not be saved in read-only mode

    Call readOnly.set(false) before saving. You can also save from the last onSave payload, or from your own mirrored state.

TypeScript
// Save editor content
const data = await editor.save();
console.log(data.blocks); // Array of block data

render(data)

Promise<void>

Renders editor content from previously saved JSON data. It accepts the loose wire shape (LooseOutputData). A null value for a block's data, id, or time from a backend DTO is normalized at the boundary.

When to use

Loads saved content and replaces the current document. To append instead of replace, use blocks.insertMany().

TypeScript
// Load saved content
const savedData = {
  blocks: [
    { id: '1', type: 'paragraph', data: { text: 'Hello' } }
  ]
};
await editor.render(savedData);

focus(atEnd?)

boolean

Sets focus to the editor. It can also put the cursor at the end of the content.

When to use

Pass true to place the caret at the very end. For a specific block or offset, use the caret API instead.

TypeScript
// Focus at start
editor.focus();

// Focus at end
editor.focus(true);

clear()

Promise<void>

Removes all content from the editor. One empty block of the default tool stays behind, so the editor is never block-less. A later save() still returns blocks: []. The blank default block does not validate, so it is dropped from the output.

When to use

Removes all blocks and leaves one empty paragraph. You can undo it, unlike destroy(), which tears the instance down.

TypeScript
// Clear all content
await editor.clear();

destroy()

void

Destroys the editor instance and removes all DOM elements and event listeners.

When to use

Call it from your framework's unmount hook to avoid leaked listeners. The instance is unusable afterwards. Create a new Blok to start again.

TypeScript
// Clean up on component unmount
editor.destroy();

handlers.set(handlers)

void

Installs, replaces or removes the live editor callbacks in place: onChange, onSave, onEnter, onSubmit, onBeforeRender, onAfterRender. Because the work happens in place, caret, selection, scroll and undo history all survive.

Only the keys you pass are touched. A key whose value is undefined UNSETS that handler. That matters because the presence of a callback is itself the semantics.

  • An onSubmit makes Enter serialize and submit instead of splitting the block.
  • An onSave arms the change-observation pipeline.

Use it to make a callback reactive without recreating the editor. The React, Vue and Angular adapters drive this setter for you when a prop, listener or [config] callback appears or disappears.

When to use

Only the keys you pass are touched. A key set to undefined unsets that handler. That is how "Enter sends" is turned back off.

Parameters

ParameterTypeRequiredDefaultDescription
handlersLiveHandlersRequired—

Partial map of live callbacks. Omitted keys are left as they are. A key set to undefined unsets that handler.

TypeScript
// "Enter sends" while composing, default Enter while editing a draft
editor.handlers.set({
  onSubmit: sendsOnEnter ? (data) => send(data) : undefined,
});

// Start mirroring content into your store, later stop again
editor.handlers.set({ onSave: (data) => store.set(data) });
editor.handlers.set({ onSave: undefined });

whenAllReady(options?)

Promise<void>

Static method. It resolves once every Blok instance in scope has finished booting, which means each instance's isReady has settled. A rejection counts as settled. It is a collective readiness signal for pages that host several instances, and it replaces hand-aggregated per-instance onReady callbacks.

  • Pass within (an Element) to count only instances mounted inside a subtree you own.
    • Then an unrelated editor elsewhere on the page cannot hold your gate closed.
  • Pass settleOn: 'rendered' to extend readiness from construction to content in the DOM, which also covers re-renders from render(data) after boot.

An empty scope resolves right away. Instances that appear while the promise is pending extend the wait. Instances constructed after it resolves are not covered, so call it again, or use subscribeReady() for a live signal.

When to use

Call it as Blok.whenAllReady() on the class, not on an instance. An instance that is still booting counts in every scope, even when its wrapper is not attached to the document yet. Over-waiting is safe. Under-waiting is a bug.

TypeScript
// A comments list: N read-only bodies + a composer.
// Wait only for the editors inside this list.
await Blok.whenAllReady({
  within: listElement,
  settleOn: 'rendered',
});
composer.focus();

readyState(options?)

{ total: number; pending: number; ready: boolean }

Static method. It gives a synchronous readiness snapshot for a scope.

The snapshot says how many instances match within. It also says how many are still pending at the requested settleOn depth, and whether the scope is settled. An empty scope reports ready: true, so you do not need a "nothing to wait for" special case.

When to use

It is cheap enough to call on every notification from subscribeReady(). It walks the registered instances and tests DOM containment.

TypeScript
const { pending, ready } = Blok.readyState({ within: listElement });

if (!ready) {
  showSkeleton(pending);
}

subscribeReady(listener)

() => void

Static method. It subscribes to readiness changes across all instances (construction, boot, render-state flip, destroy) and returns an unsubscribe function. The listener takes no arguments, so re-read Blok.readyState(scope) when it fires. It works with useSyncExternalStore and other store adapters, and gives a live signal instead of a one-shot latch.

When to use

In a framework, prefer the adapter wrappers: useBlokReady() in @bloklabs/react and @bloklabs/vue, injectBlokReady() in @bloklabs/angular. They wrap this subscription and the scope lookup.

TypeScript
const unsubscribe = Blok.subscribeReady(() => {
  setReady(Blok.readyState({ within: listElement }).ready);
});

// later
unsubscribe();

createSelector(attr, value?)

string

A named export of the package root, not a member of the Blok class. It builds a CSS selector from a DATA_ATTR value.

  • With no value it produces a presence selector.
  • With one it produces an equality selector.

Use it together with Blok.DATA_ATTR instead of matching Blok's internal class names, which are not part of the public surface.

TypeScript
import { DATA_ATTR, createSelector } from '@bloklabs/core';

createSelector(DATA_ATTR.element); // '[data-blok-element]'
document.querySelectorAll(createSelector(DATA_ATTR.selected, true));

icons

string

Named exports of the @bloklabs/core/icons subpath, not members of the Blok class. They are Blok's own glyphs as SVG strings, one Icon* constant per glyph (IconBold, IconPlus, IconTrash, IconWarning, …).

They are plain strings, so they drop straight into the places a tool must supply markup: a tool's static get toolbox() icon and the entries returned by renderSettings(). The subpath ships a generated, self-contained declaration file (types/icons.d.ts) that lists every constant, so editor autocomplete is the reference for the full set.

TypeScript
import { IconBold, IconPlus } from '@bloklabs/core/icons';

class Callout {
  static get toolbox() {
    return { title: 'Callout', icon: IconPlus };
  }

  renderSettings() {
    return [{ icon: IconBold, title: 'Bold text', onActivate: () => this.toggleBold() }];
  }
}

Properties

PropertyTypeDescription
DATA_ATTRRecord<DataAttrKey, DataAttrValue>

A named export of the package root (import { DATA_ATTR } from '@bloklabs/core'), not a property of the editor instance. It holds the stable data-blok-* attribute names Blok writes on its DOM.

This is the supported way to query editor DOM and write host tests, instead of matching internal class names. The DataAttrKey / DataAttrValue types and the createSelector() helper are exported alongside it.

BLOK_FONT_SIZE_TOKENSBlokFontSizeTokens

A named export of the package root (import { BLOK_FONT_SIZE_TOKENS } from '@bloklabs/core'), not a property of the editor instance. It gives the CSS custom property each style.fontSize scenario writes, in a map shaped exactly like the config itself (BLOK_FONT_SIZE_TOKENS.paragraph, .heading[1], .list.checklist, .bookmark.link, …).

Use it wherever typography is driven by a channel other than the constructor config: a per-region CSS rule, or editor.tokens.set({ [BLOK_FONT_SIZE_TOKENS.paragraph]: '18px' }) at runtime. Then the custom property names never have to be hand-copied, and a rename is a compile error instead of a silent no-op.

versionstring

A named export of the package root (import { version } from '@bloklabs/core'), not a property of the editor instance. It is the running editor version, the same value stamped into OutputData.version.

PendingBlok{ isReady; isRendered; destroy(); theme; width; placeholder; tokens; i18n }

A type exported from the package root (import type { PendingBlok } from '@bloklabs/core'), not a property of the editor instance.

It is the surface guaranteed to exist synchronously between new Blok(config) and isReady resolving. Blok builds its module APIs (blocks, caret, history, readOnly, …) asynchronously, so reading them earlier returns undefined.

PendingBlok declares only the eight members listed here. That turns the window into a compile error instead of an undefined at runtime: const pending: PendingBlok = new Blok(config); const editor = await pending.isReady;.

isReadyPromise<Blok>

Promise that resolves with the ready editor instance. The API namespaces below (blocks, caret, history, readOnly, …) are built asynchronously. They are undefined until it resolves, so type the reference you hold during that window as PendingBlok.

isRenderedboolean

Synchronous render-readiness flag. It is true once the current render batch has landed in the DOM, and it mirrors the data-blok-rendered wrapper attribute. It is false before the first render and while a re-render is in flight. It complements the async isReady and onReady: no await or callback is needed, so mount state can be polled synchronously.

blocksBlocks

Blocks API module

caretCaret

Caret API module

historyHistory

History API module

saverSaver

Saver API module

toolbarToolbar

Toolbar API module

inlineToolbarInlineToolbar

Inline toolbar API module

toolsTools

Tools API module

uploaderUploader

Uploader API module for asset uploads routed by asset kind

eventsEvents

Events API module

listenersListeners

Listeners API module

notifierNotifier

Notifier API module

sanitizerSanitizer

Sanitizer API module

selectionSelection

Selection API module

marksMarks

Marks API module for range-aware inline-mark operations

stylesStyles

Styles API module

tooltipTooltip

Tooltip API module

readOnlyReadOnly

ReadOnly API module

uiUi

UI API module

themeTheme

Theme API module

widthWidth

Width API module

placeholderPlaceholder

Placeholder API module

tokensTokens

Runtime theme-tokens API module

i18nEditorI18n

I18n API module. It has everything a tool gets through api.i18n, plus update()

configReadonly<Pick<BlokConfig, 'linkPaste' | 'link'>>

Read-only view of selected editor configuration: the link and linkPaste options this instance was created with. A custom inline or link tool reads the host's link policy from here (as api.config) instead of working it out again.

rectangleSelection{ cancelActiveSelection(): void; isRectActivated(): boolean; clearSelection(): void; startSelection(pageX: number, pageY: number, shiftKey?: boolean): void; endSelection(): void }

Drag-select (rubber-band) control. It is also reachable inside a tool as api.rectangleSelection.

  • startSelection(pageX, pageY, shiftKey?) begins a rubber-band from page coordinates.
  • endSelection() resets the drag state and hides the overlay.
  • isRectActivated() reports whether a rubber-band is currently active.
  • clearSelection() drops the active flag.
  • cancelActiveSelection() aborts a selection in progress (clear + end).

Another selection system, for example table cell selection, calls it when it takes priority.