CoTexts
Jazz provides two CoValue types for collaborative text editing, collectively referred to as "CoText" values:
- CoPlainText for simple text editing without formatting
- CoRichText for rich text with HTML-based formatting (extends CoPlainText)
Both types enable real-time collaborative editing of text content while maintaining consistency across multiple users.
Note: If you're looking for a quick way to add rich text editing to your app, check out jazz-richtext-prosemirror.
const
const note: CoPlainText
note =class CoPlainText
CoPlainText.
CoPlainText.create<CoPlainText>(this: CoValueClass<...>, text: string, options?: { owner: Account | Group; } | Account | Group): CoPlainText
Create a new `CoPlainText` with the given text and owner. The owner (a Group or Account) determines access rights to the CoPlainText. The CoPlainText will immediately be persisted and synced to connected peers.create("Meeting notes", {owner: Account | Group
owner:const me: Account
me }); // Update the textconst note: CoPlainText
note.CoPlainText.applyDiff(other: string): void
Apply text, modifying the text in place. Calculates the diff and applies it to the CoValue.applyDiff("Meeting notes for Tuesday");var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.log(const note: CoPlainText
note.CoPlainText.toString(): string
Returns a string representation of a string.toString()); // "Meeting notes for Tuesday"
For a full example of CoTexts in action, see our Richtext example app, which shows plain text and rich text editing.
CoPlainText vs co.string
While co.string
is perfect for simple text fields, CoPlainText
is the right choice when you need:
- Multiple users editing the same text simultaneously
- Fine-grained control over text edits (inserting, deleting at specific positions)
- Character-by-character collaboration
- Efficient merging of concurrent changes
Both support real-time updates, but CoPlainText
provides specialized tools for collaborative editing scenarios.
Creating CoText Values
CoText values are typically used as fields in your schemas:
class
class Profile
Profile extendsclass CoMap
CoMaps are collaborative versions of plain objects, mapping string-like keys to values.CoMap {Profile.name: co<string>
name =co.
const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }
string: co<string>
string;Profile.bio: co<CoPlainText | null>
bio =co.
const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }
ref: <typeof CoPlainText>(arg: typeof CoPlainText | ((_raw: RawCoPlainText<JsonObject | null>) => typeof CoPlainText), options?: never) => co<...> (+1 overload)
ref(class CoPlainText
CoPlainText); // Plain text fieldProfile.description: co<CoRichText | null>
description =co.
const co: { string: co<string>; number: co<number>; boolean: co<boolean>; null: co<null>; Date: co<Date>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number]>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T>; encoded<T>(arg: Encoder<T>): co<T>; ref: { ...; }; items: ItemsSym; optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; json<T extends CojsonInternalTypes.CoJsonValue<T>>(): co<T | undefined>; encoded<T>(arg: OptionalEncoder<T>): co<T | undefined>; string: co<string | undefined>; number: co<number | undefined>; boolean: co<boolean | undefined>; null: co<null | undefined>; Date: co<Date | undefined>; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }; }
ref: <typeof CoRichText>(arg: typeof CoRichText | ((_raw: RawCoPlainText<JsonObject | null>) => typeof CoRichText), options?: never) => co<...> (+1 overload)
ref(class CoRichText
CoRichText); // Rich text with formatting }
Create a CoText value with a simple string:
// Create plaintext with default ownership (current user) const
const note: CoPlainText
note =class CoPlainText
CoPlainText.
CoPlainText.create<CoPlainText>(this: CoValueClass<CoPlainText>, text: string, options?: { owner: Account | Group; } | Account | Group): CoPlainText
Create a new `CoPlainText` with the given text and owner. The owner (a Group or Account) determines access rights to the CoPlainText. The CoPlainText will immediately be persisted and synced to connected peers.create("Meeting notes", {owner: Account | Group
owner:const me: Account
me }); // Create rich text with HTML content constconst document: CoRichText
document =class CoRichText
CoRichText.
CoPlainText.create<CoRichText>(this: CoValueClass<CoRichText>, text: string, options?: { owner: Account | Group; } | Account | Group): CoRichText
Create a new `CoPlainText` with the given text and owner. The owner (a Group or Account) determines access rights to the CoPlainText. The CoPlainText will immediately be persisted and synced to connected peers.create("<p>Project <strong>overview</strong></p>", {owner: Account | Group
owner:const me: Account
me } );
Ownership
Like other CoValues, you can specify ownership when creating CoTexts.
// Create with shared ownership const
const teamGroup: Group
teamGroup =class Group
Group.create();
Group.create<Group>(this: CoValueClass<Group>, options?: { owner: Account; } | Account): Group
const teamGroup: Group
teamGroup.Group.addMember(member: Account, role: AccountRole): void (+1 overload)
addMember(const colleagueAccount: Account
colleagueAccount, "writer"); constconst teamNote: CoPlainText
teamNote =class CoPlainText
CoPlainText.
CoPlainText.create<CoPlainText>(this: CoValueClass<CoPlainText>, text: string, options?: { owner: Account | Group; } | Account | Group): CoPlainText
Create a new `CoPlainText` with the given text and owner. The owner (a Group or Account) determines access rights to the CoPlainText. The CoPlainText will immediately be persisted and synced to connected peers.create("Team updates", {owner: Group | Account
owner:const teamGroup: Group
teamGroup });
See Groups as permission scopes for more information on how to use groups to control access to CoText values.
Reading Text
CoText values work similarly to JavaScript strings:
// Get the text content
var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.log(const note: CoPlainText
note.CoPlainText.toString(): string
Returns a string representation of a string.toString()); // "Meeting notes"var console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.log(`${const note: CoPlainText
note}`); // "Meeting notes" // Check the text lengthvar console: Console
The `console` module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers. The module exports two specific components: * A `Console` class with methods such as `console.log()`, `console.error()` and `console.warn()` that can be used to write to any Node.js stream. * A global `console` instance configured to write to [`process.stdout`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstdout) and [`process.stderr`](https://nodejs.org/docs/latest-v20.x/api/process.html#processstderr). The global `console` can be used without importing the `node:console` module. _**Warning**_: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the [`note on process I/O`](https://nodejs.org/docs/latest-v20.x/api/process.html#a-note-on-process-io) for more information. Example using the global `console`: ```js console.log('hello world'); // Prints: hello world, to stdout console.log('hello %s', 'world'); // Prints: hello world, to stdout console.error(new Error('Whoops, something bad happened')); // Prints error message and stack trace to stderr: // Error: Whoops, something bad happened // at [eval]:5:15 // at Script.runInThisContext (node:vm:132:18) // at Object.runInThisContext (node:vm:309:38) // at node:internal/process/execution:77:19 // at [eval]-wrapper:6:22 // at evalScript (node:internal/process/execution:76:60) // at node:internal/main/eval_string:23:3 const name = 'Will Robinson'; console.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ``` Example using the `Console` class: ```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err); myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err const name = 'Will Robinson'; myConsole.warn(`Danger ${name}! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```console.Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)
Prints to `stdout` with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to [`printf(3)`](http://man7.org/linux/man-pages/man3/printf.3.html) (the arguments are all passed to [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args)). ```js const count = 5; console.log('count: %d', count); // Prints: count: 5, to stdout console.log('count:', count); // Prints: count: 5, to stdout ``` See [`util.format()`](https://nodejs.org/docs/latest-v20.x/api/util.html#utilformatformat-args) for more information.log(const note: CoPlainText
note.CoPlainText.length: number
Returns the length of a String object.length); // 14
Making Edits
Insert and delete text with intuitive methods:
// Insert text at a specific position
const note: CoPlainText
note.CoPlainText.insertBefore(idx: number, text: string): void
insertBefore(8, "weekly "); // "Meeting weekly notes" // Insert after a positionconst note: CoPlainText
note.CoPlainText.insertAfter(idx: number, text: string): void
insertAfter(21, " for Monday"); // "Meeting weekly notes for Monday" // Delete a range of textconst note: CoPlainText
note.deleteRange({
CoPlainText.deleteRange(range: { from: number; to: number; }): void
from: number
from: 8,to: number
to: 15 }); // "Meeting notes for Monday" // Apply a diff to update the entire textconst note: CoPlainText
note.CoPlainText.applyDiff(other: string): void
Apply text, modifying the text in place. Calculates the diff and applies it to the CoValue.applyDiff("Team meeting notes for Tuesday");
Applying Diffs
Use applyDiff
to efficiently update text with minimal changes:
// Original text: "Team status update" const
const minutes: CoPlainText
minutes =class CoPlainText
CoPlainText.
CoPlainText.create<CoPlainText>(this: CoValueClass<CoPlainText>, text: string, options?: { owner: Account | Group; } | Account | Group): CoPlainText
Create a new `CoPlainText` with the given text and owner. The owner (a Group or Account) determines access rights to the CoPlainText. The CoPlainText will immediately be persisted and synced to connected peers.create("Team status update", {owner: Account | Group
owner:const me: Account
me }); // Replace the entire text with a new versionconst minutes: CoPlainText
minutes.CoPlainText.applyDiff(other: string): void
Apply text, modifying the text in place. Calculates the diff and applies it to the CoValue.applyDiff("Weekly team status update for Project X"); // Make partial changes letlet text: string
text =const minutes: CoPlainText
minutes.CoPlainText.toString(): string
Returns a string representation of a string.toString();let text: string
text =let text: string
text.String.replace(searchValue: string | RegExp, replaceValue: string): string (+3 overloads)
Replaces text in a string, using a regular expression or search string.replace("Weekly", "Monday");const minutes: CoPlainText
minutes.CoPlainText.applyDiff(other: string): void
Apply text, modifying the text in place. Calculates the diff and applies it to the CoValue.applyDiff(let text: string
text); // Efficiently updates only what changed
Perfect for handling user input in form controls:
<script setup lang="ts"> import { ref, onMounted } from 'vue'; import { CoPlainText } from "jazz-tools"; import { createJazzTestAccount } from 'jazz-tools/testing'; const note = ref(null); const textContent = ref(""); onMounted(async () => { const me = await createJazzTestAccount(); note.value = CoPlainText.create("", { owner: me }); textContent.value = note.value.toString(); }); function updateText(e) { if (note.value) { note.value.applyDiff(e.target.value); textContent.value = note.value.toString(); } } </script> <template> <textarea :value="textContent" @input="updateText" /> </template>
Using Rich Text with ProseMirror
Jazz provides a dedicated plugin for integrating CoRichText with the popular ProseMirror editor. This plugin, jazz-richtext-prosemirror
, enables bidirectional synchronization between your CoRichText instances and ProseMirror editors.
ProseMirror Plugin Features
- Bidirectional Sync: Changes in the editor automatically update the CoRichText and vice versa
- Real-time Collaboration: Multiple users can edit the same document simultaneously
- HTML Conversion: Automatically converts between HTML (used by CoRichText) and ProseMirror's document model
Installation
pnpm add jazz-richtext-prosemirror \ prosemirror-view \ prosemirror-state \ prosemirror-schema-basic
Integration
We don't currently have a Vue-specific example, but you need help you can request one, or ask on Discord.
For use without a framework:
import { CoRichText } from "jazz-tools"; import { createJazzPlugin } from "jazz-richtext-prosemirror"; import { exampleSetup } from "prosemirror-example-setup"; import { schema } from "prosemirror-schema-basic"; import { EditorState } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; function setupRichTextEditor(coRichText, container) { // Create the Jazz plugin for ProseMirror // Providing a CoRichText instance to the plugin to automatically sync changes const jazzPlugin = createJazzPlugin(coRichText); // Set up ProseMirror with Jazz plugin const view = new EditorView(container, { state: EditorState.create({ schema, plugins: [ ...exampleSetup({ schema }), jazzPlugin, ], }), }); // Return cleanup function return () => { view.destroy(); }; } // Usage const document = CoRichText.create("<p>Initial content</p>", { owner: me }); const editorContainer = document.getElementById("editor"); const cleanup = setupRichTextEditor(document, editorContainer); // Later when done with the editor cleanup();