Write a behavior extension
Learn options, storage, commands and lifecycle through a small greeting command.
Suppose your product needs an “Insert greeting” button. It inserts ordinary text without introducing document structure, so use Extension. Start with an Editor containing StarterKit; complete the quick start first if needed.
1. Separate configuration, runtime state and content
| Data | Owner | Saved in JSON? |
|---|---|---|
| Greeting to insert | addOptions(), overridden through .configure() | No |
| Content update count for this session | addStorage() | No |
| Inserted text | Document produced by the command | Yes |
Persistent business data belongs in node or mark attributes, rather than storage. An extension object caches its storage. Create a fresh extension for each Editor to avoid sharing mutable state between editors.
2. Define a typed command
createGreeting() defines insertGreeting(). It delegates to insertContent() with a text node, so even <b>Hello</b> is literal text rather than interpreted HTML.
import { Extension } from "@quirekit/core";
import type { GreetingOptions, GreetingStorage } from "./types";
declare module "@quirekit/core" {
/** Application commands installed by the greeting extension. */
interface Commands<ReturnType> {
/** Greeting behavior. */
greeting: {
/** @returns Whether insertion can run or was accepted. */
insertGreeting: () => ReturnType;
};
}
}
/**
* Creates fresh options/storage for each editor instead of sharing a mutable extension.
* @param text - Literal greeting, never interpreted as HTML.
* @returns One installable, configured behavior.
*/
export function createGreeting(text = "Hello!") {
return Extension.create<GreetingOptions, GreetingStorage>({
name: "greeting",
/** @returns Application defaults. */
addOptions() { return { text: "Hello!" }; },
/** @returns Session-local state; it is not document JSON. */
addStorage() { return { updates: 0 }; },
/** @returns The same command for direct, chain and can modes. */
addCommands() {
return {
insertGreeting: () => ({ commands }) => this.options.text.length > 0
&& commands.insertContent({ type: "text", text: this.options.text }),
};
},
/** Counts accepted document updates; querying can() does not run this hook. */
onUpdate() { this.storage.updates += 1; },
}).configure({ text });
}
The module augmentation adds the same command type to editor.commands, chain() and can(). Types alone do not install behavior: the extension must be present at runtime.
3. Install and call it
extensions: [StarterKit, createGreeting('Hello!')]
On a button click, call editor.chain().focus().insertGreeting().run(). Query availability with editor.can().insertGreeting(). Focus does not establish permission; the application still checks whether the user may edit.
Commands must support can() without requests, storage mutations or DOM effects. This recipe delegates insertion to an existing command and counts accepted updates in onUpdate. Availability queries do not increase the counter.
4. Configuration and inheritance
.configure({ text: 'Welcome' }) changes public options. .extend({ ... }) changes the definition, for example a keyboard binding. When overriding a hook that still needs its inherited behavior, call that hook's this.parent?.().
Install only the final extension. Installing both the original and a same-name extended version causes name, Schema or command conflicts. Higher priority runs earlier; do not use ordering to conceal competing owners of one feature.
5. Verify the result
After can(), both content and storage should be unchanged. Executing the command inserts text; undo removes it. Two Editors with separate createGreeting() results keep separate counters. The counter never appears in downloaded JSON.
Compose complex commands through their supplied tr, commands and chain, without starting a separate editor.commands transaction inside them. A later false result does not roll back earlier writes; see commands and transactions.
Continue with input rules and state plugins, or add persistent nodes and layout. Consult the extension API for complete hooks.