How to write autosaving forms to create and update CoValues
This guide shows you a simple and powerful way to implement forms for creating and updating CoValues.
We'll build:
- An update form that saves changes as you make them, removing the need for a save button.
- A create form that autosaves your changes into a draft, so you can come back to it later.
Note: If you do need a save button on your update form, this guide is not for you. Another option is to use react-hook-form.
Updating a CoValue
To update a CoValue, we simply assign the new value directly as changes happen. These changes are synced to the server.
<input type="text" value={order.name} onChange={(e) => order.$jazz.set("name", e.target.value)} />
It's that simple!
Creating a CoValue
However, when creating a CoValue, the CoValue does not exist yet, so we don't have the advantages previously mentioned.
There's a way around this, and it provides unexpected benefits too.
Using a Draft CoValue
Let's say we have a CoValue called BubbleTeaOrder
. We can create a "draft" CoValue,
which is an empty version of a BubbleTeaOrder
, that we can then modify when we are "creating"
a new CoValue.
A DraftBubbleTeaOrder
is essentially a copy of BubbleTeaOrder
, but with all the fields made optional.
// schema.ts export const
BubbleTeaOrder =
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
import co
co.map({
map<{ name: z.z.ZodString; }>(shape: { name: z.z.ZodString; }): co.Map<{ name: z.z.ZodString; }, unknown, Account | Group> export map
name: z.z.ZodString
name:import z
z.string(), }); export type
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export string
BubbleTeaOrder =
type BubbleTeaOrder = { readonly name: string; } & CoMap
import co
co.loaded<typeof
type loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loaded
BubbleTeaOrder>; export const
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
DraftBubbleTeaOrder =
const DraftBubbleTeaOrder: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
BubbleTeaOrder.
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
CoMapSchema<{ name: ZodString; }, unknown, Account | Group>.partial(): co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
Creates a new CoMap schema by making all fields optional.partial(); export typeDraftBubbleTeaOrder =
type DraftBubbleTeaOrder = { readonly name: string | undefined; } & CoMap
import co
co.loaded<typeof
type loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loaded
DraftBubbleTeaOrder>;
const DraftBubbleTeaOrder: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
Writing the components in React
Let's write the form component that will be used for both create and update.
// OrderForm.tsx export function
OrderForm({
function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | DraftBubbleTeaOrder; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element
order,
order: ({ readonly name: string; } & CoMap) | ({ readonly name: string | undefined; } & CoMap)
onSave: ((e: React.FormEvent<HTMLFormElement>) => void) | undefined
onSave, }: {order:
order: ({ readonly name: string; } & CoMap) | ({ readonly name: string | undefined; } & CoMap)
BubbleTeaOrder |
type BubbleTeaOrder = { readonly name: string; } & CoMap
DraftBubbleTeaOrder;
type DraftBubbleTeaOrder = { readonly name: string | undefined; } & CoMap
onSave?: ((e: React.FormEvent<HTMLFormElement>) => void) | undefined
onSave?: (e: React.FormEvent<HTMLFormElement>
e: React.interface React.FormEvent<T = Element>
FormEvent<HTMLFormElement>) => void; }) { return ( <React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>
formReact.DOMAttributes<HTMLFormElement>.onSubmit?: React.FormEventHandler<HTMLFormElement> | undefined
onSubmit={onSave: ((e: React.FormEvent<HTMLFormElement>) => void) | undefined
onSave}> <React.JSX.IntrinsicElements.label: React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>
label> Name <React.JSX.IntrinsicElements.input: React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>
inputReact.InputHTMLAttributes<HTMLInputElement>.type?: React.HTMLInputTypeAttribute | undefined
type="text"React.InputHTMLAttributes<HTMLInputElement>.value?: string | number | readonly string[] | undefined
value={order.
order: ({ readonly name: string; } & CoMap) | ({ readonly name: string | undefined; } & CoMap)
name: string | undefined
name}React.InputHTMLAttributes<HTMLInputElement>.onChange?: React.ChangeEventHandler<HTMLInputElement> | undefined
onChange={(e: React.ChangeEvent<HTMLInputElement>
e) =>order.
order: ({ readonly name: string; } & CoMap) | ({ readonly name: string | undefined; } & CoMap)
CoMap.$jazz: CoMapJazzApi<{ readonly name: string; } & CoMap> | CoMapJazzApi<{ readonly name: string | undefined; } & CoMap>
Jazz methods for CoMaps are inside this property. This allows CoMaps to be used as plain objects while still having access to Jazz methods, and also doesn't limit which key names can be used inside CoMaps.$jazz.CoMapJazzApi<M extends CoMap>.set<"name">(key: "name", value: string): void
Set a value on the CoMapset("name",e: React.ChangeEvent<HTMLInputElement>
e.React.ChangeEvent<HTMLInputElement>.target: EventTarget & HTMLInputElement
target.HTMLInputElement.value: string
Returns the value of the data at the cursor's current position.value)}React.InputHTMLAttributes<HTMLInputElement>.required?: boolean | undefined
required /> </React.JSX.IntrinsicElements.label: React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>
label> {onSave: ((e: React.FormEvent<HTMLFormElement>) => void) | undefined
onSave && <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
buttonReact.ButtonHTMLAttributes<HTMLButtonElement>.type?: "button" | "reset" | "submit" | undefined
type="submit">Submit</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button>} </React.JSX.IntrinsicElements.form: React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>
form> ); }
Writing the edit form
To make the edit form, simply pass the BubbleTeaOrder
.
// EditOrder.tsx export function
EditOrder(
function EditOrder(props: { id: string; }): React.JSX.Element | undefined
props: {
props: { id: string; }
id: string
id: string }) { constorder =
const order: ({ readonly name: string; } & CoMap) | null | undefined
useCoState<co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>, true>(Schema: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>, id: string | undefined, options?: { ...; } | undefined): ({ ...; } & CoMap) | ... 1 more ... | undefined
React hook for subscribing to CoValues and handling loading states. This hook provides a convenient way to subscribe to CoValues and automatically handles the subscription lifecycle (subscribe on mount, unsubscribe on unmount). It also supports deep loading of nested CoValues through resolve queries.useCoState(BubbleTeaOrder,
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
props.
props: { id: string; }
id: string
id); if (!order) return; return <
const order: ({ readonly name: string; } & CoMap) | null | undefined
OrderForm
function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | DraftBubbleTeaOrder; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element
order={
order: ({ readonly name: string; } & CoMap) | ({ readonly name: string | undefined; } & CoMap)
order} />; }
const order: { readonly name: string; } & CoMap
Writing the create form
For the create form, we need to:
- Create a draft order.
- Edit the draft order.
- Convert the draft order to a "real" order on submit.
Here's how that looks like:
// CreateOrder.tsx export function
function CreateOrder(): React.JSX.Element | undefined
CreateOrder() { const {me } =
const me: NonNullable<Account | ({ readonly [x: string]: any; } & Account) | null> | null | undefined
useAccount<CoreAccountSchema<z.z.core.$ZodLooseShape> | AccountClass<Account>, true>(AccountSchema?: CoreAccountSchema<z.z.core.$ZodLooseShape> | AccountClass<...> | undefined, options?: { ...; } | undefined): { ...; }
React hook for accessing the current user's account and authentication state. This hook provides access to the current user's account profile and root data, along with authentication utilities. It automatically handles subscription to the user's account data and provides a logout function.useAccount(); const [draft,
const draft: ({ readonly name: string | undefined; } & CoMap) | undefined
setDraft] =
const setDraft: React.Dispatch<React.SetStateAction<({ readonly name: string | undefined; } & CoMap) | undefined>>
useState<{ readonly name: string | undefined; } & CoMap>(): [({ readonly name: string | undefined; } & CoMap) | undefined, React.Dispatch<React.SetStateAction<({ readonly name: string | undefined; } & CoMap) | undefined>>] (+1 overload)
Returns a stateful value, and a function to update it.useState<DraftBubbleTeaOrder>();
type DraftBubbleTeaOrder = { readonly name: string | undefined; } & CoMap
function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void
Accepts a function that contains imperative, possibly effectful code.useEffect(() => {setDraft(
const setDraft: (value: React.SetStateAction<({ readonly name: string | undefined; } & CoMap) | undefined>) => void
DraftBubbleTeaOrder.
const DraftBubbleTeaOrder: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
create({})); }, [
CoMapSchema<{ name: ZodOptional<ZodString>; }, unknown, Account | Group>.create(init: { name?: string | undefined; }, options?: { owner?: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): { ...; } & CoMap (+1 overload)
me?.
const me: NonNullable<Account | ({ readonly [x: string]: any; } & Account) | null> | null | undefined
Account.$jazz: AccountJazzApi<Account> | AccountJazzApi<{ readonly [x: string]: any; } & Account>
Jazz methods for Accounts are inside this property. This allows Accounts to be used as plain objects while still having access to Jazz methods.$jazz.AccountJazzApi<A extends Account>.id: string | undefined
The ID of this `Account`id]); constconst onSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave = (e: React.FormEvent<HTMLFormElement>
e: React.interface React.FormEvent<T = Element>
FormEvent<HTMLFormElement>) => {e: React.FormEvent<HTMLFormElement>
e.React.BaseSyntheticEvent<Event, EventTarget & HTMLFormElement, EventTarget>.preventDefault(): void
preventDefault(); if (!draft || !
const draft: ({ readonly name: string | undefined; } & CoMap) | undefined
draft.
const draft: { readonly name: string | undefined; } & CoMap
name: string | undefined
name) return; constorder =
const order: { readonly name: string; } & CoMap
draft as
const draft: { readonly name: string | undefined; } & CoMap
BubbleTeaOrder; // TODO: this should narrow correctly
type BubbleTeaOrder = { readonly name: string; } & CoMap
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 (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log("Order created:",order); }; if (!
const order: { readonly name: string; } & CoMap
draft) return; return <
const draft: ({ readonly name: string | undefined; } & CoMap) | undefined
OrderForm
function OrderForm({ order, onSave, }: { order: co.loaded<typeof BubbleTeaOrder> | co.loaded<typeof DraftBubbleTeaOrder>; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element
order={
order: ({ readonly name: string | undefined; } & CoMap) | ({ readonly name: string; } & CoMap)
draft}
const draft: { readonly name: string | undefined; } & CoMap
onSave?: ((e: React.FormEvent<HTMLFormElement>) => void) | undefined
onSave={const onSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave} />; }
Validation
In a BubbleTeaOrder
, the name
field is required, so it would be a good idea to validate this before turning the draft into a real order.
Update the schema to include a validateDraftOrder
helper.
// schema.ts export const
BubbleTeaOrder =
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
import co
co.map({
map<{ name: z.z.ZodString; }>(shape: { name: z.z.ZodString; }): co.Map<{ name: z.z.ZodString; }, unknown, Account | Group> export map
name: z.z.ZodString
name:import z
z.string(), }); export type
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export string
BubbleTeaOrder =
type BubbleTeaOrder = { readonly name: string; } & CoMap
import co
co.loaded<typeof
type loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loaded
BubbleTeaOrder>; export const
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
DraftBubbleTeaOrder =
const DraftBubbleTeaOrder: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
BubbleTeaOrder.
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
CoMapSchema<{ name: ZodString; }, unknown, Account | Group>.partial(): co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
Creates a new CoMap schema by making all fields optional.partial(); export typeDraftBubbleTeaOrder =
type DraftBubbleTeaOrder = { readonly name: string | undefined; } & CoMap
import co
co.loaded<typeof
type loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loaded
DraftBubbleTeaOrder>; export function
const DraftBubbleTeaOrder: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
validateDraftOrder(
function validateDraftOrder(draft: DraftBubbleTeaOrder): { errors: string[]; }
draft:
draft: { readonly name: string | undefined; } & CoMap
DraftBubbleTeaOrder) { const
type DraftBubbleTeaOrder = { readonly name: string | undefined; } & CoMap
const errors: string[]
errors: string[] = []; if (!draft.
draft: { readonly name: string | undefined; } & CoMap
name: string | undefined
name) {const errors: string[]
errors.Array<string>.push(...items: string[]): number
Appends new elements to the end of an array, and returns the new length of the array.push("Please enter a name."); } return {errors: string[]
errors }; };
Then perform the validation on submit.
// CreateOrder.tsx export function
function CreateOrder(): React.JSX.Element | undefined
CreateOrder() { const {me } =
const me: NonNullable<Account | ({ readonly [x: string]: any; } & Account) | null> | null | undefined
useAccount<CoreAccountSchema<z.z.core.$ZodLooseShape> | AccountClass<Account>, true>(AccountSchema?: CoreAccountSchema<z.z.core.$ZodLooseShape> | AccountClass<...> | undefined, options?: { ...; } | undefined): { ...; }
React hook for accessing the current user's account and authentication state. This hook provides access to the current user's account profile and root data, along with authentication utilities. It automatically handles subscription to the user's account data and provides a logout function.useAccount(); const [draft,
const draft: ({ readonly name: string | undefined; } & CoMap) | undefined
setDraft] =
const setDraft: React.Dispatch<React.SetStateAction<({ readonly name: string | undefined; } & CoMap) | undefined>>
useState<{ readonly name: string | undefined; } & CoMap>(): [({ readonly name: string | undefined; } & CoMap) | undefined, React.Dispatch<React.SetStateAction<({ readonly name: string | undefined; } & CoMap) | undefined>>] (+1 overload)
Returns a stateful value, and a function to update it.useState<DraftBubbleTeaOrder>();
type DraftBubbleTeaOrder = { readonly name: string | undefined; } & CoMap
function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void
Accepts a function that contains imperative, possibly effectful code.useEffect(() => {setDraft(
const setDraft: (value: React.SetStateAction<({ readonly name: string | undefined; } & CoMap) | undefined>) => void
DraftBubbleTeaOrder.
const DraftBubbleTeaOrder: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
create({})); }, [
CoMapSchema<{ name: ZodOptional<ZodString>; }, unknown, Account | Group>.create(init: { name?: string | undefined; }, options?: { owner?: Group; unique?: CoValueUniqueness["uniqueness"]; } | Group): { ...; } & CoMap (+1 overload)
me?.
const me: NonNullable<Account | ({ readonly [x: string]: any; } & Account) | null> | null | undefined
Account.$jazz: AccountJazzApi<Account> | AccountJazzApi<{ readonly [x: string]: any; } & Account>
Jazz methods for Accounts are inside this property. This allows Accounts to be used as plain objects while still having access to Jazz methods.$jazz.AccountJazzApi<A extends Account>.id: string | undefined
The ID of this `Account`id]); constconst onSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave = (e: React.FormEvent<HTMLFormElement>
e: React.interface React.FormEvent<T = Element>
FormEvent<HTMLFormElement>) => {e: React.FormEvent<HTMLFormElement>
e.React.BaseSyntheticEvent<Event, EventTarget & HTMLFormElement, EventTarget>.preventDefault(): void
preventDefault(); if (!draft) return; const
const draft: ({ readonly name: string | undefined; } & CoMap) | undefined
validation =
const validation: { errors: string[]; }
validateDraftOrder(
function validateDraftOrder(draft: DraftBubbleTeaOrder): { errors: string[]; }
draft); if (
const draft: { readonly name: string | undefined; } & CoMap
validation.
const validation: { errors: string[]; }
errors: string[]
errors.Array<T>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length > 0) {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 (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(validation.
const validation: { errors: string[]; }
errors: string[]
errors); return; } constorder =
const order: { readonly name: string; } & CoMap
draft as
const draft: { readonly name: string | undefined; } & CoMap
BubbleTeaOrder;
type BubbleTeaOrder = { readonly name: string; } & CoMap
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 (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log("Order created:",order); }; if (!
const order: { readonly name: string; } & CoMap
draft) return; return <
const draft: ({ readonly name: string | undefined; } & CoMap) | undefined
OrderForm
function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | DraftBubbleTeaOrder; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element
order={
order: ({ readonly name: string | undefined; } & CoMap) | ({ readonly name: string; } & CoMap)
draft}
const draft: { readonly name: string | undefined; } & CoMap
onSave?: ((e: React.FormEvent<HTMLFormElement>) => void) | undefined
onSave={const onSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave} />; }
Saving the user's work-in-progress
It turns out that using this pattern also provides a UX improvement.
By storing the draft in the user's account, they can come back to it anytime without losing their work. 🙌
// schema.ts export const
BubbleTeaOrder =
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
import co
co.map({
map<{ name: z.z.ZodString; }>(shape: { name: z.z.ZodString; }): co.Map<{ name: z.z.ZodString; }, unknown, Account | Group> export map
name: z.z.ZodString
name:import z
z.string(), }); export type
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export string
BubbleTeaOrder =
type BubbleTeaOrder = { readonly name: string; } & CoMap
import co
co.loaded<typeof
type loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loaded
BubbleTeaOrder>; export const
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
DraftBubbleTeaOrder =
const DraftBubbleTeaOrder: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
BubbleTeaOrder.
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
CoMapSchema<{ name: ZodString; }, unknown, Account | Group>.partial(): co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
Creates a new CoMap schema by making all fields optional.partial(); export typeDraftBubbleTeaOrder =
type DraftBubbleTeaOrder = { readonly name: string | undefined; } & CoMap
import co
co.loaded<typeof
type loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loaded
DraftBubbleTeaOrder>; export const
const DraftBubbleTeaOrder: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
AccountRoot =
const AccountRoot: co.Map<{ draft: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>
import co
co.map({
map<{ draft: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>; }>(shape: { draft: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>; }): co.Map<...> export map
draft:
draft: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
DraftBubbleTeaOrder, }); export const
const DraftBubbleTeaOrder: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
JazzAccount =
const JazzAccount: co.Account<{ root: co.Map<{ draft: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>; profile: co.Map<{ ...; }, unknown, Account | Group>; }>
import co
co.
account<{ root: co.Map<{ draft: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>; profile: co.Map<...>; }>(shape?: { ...; } | undefined): co.Account<...> export account
Defines a collaborative account schema for Jazz applications. Creates an account schema that represents a user account with profile and root data. Accounts are the primary way to identify and manage users in Jazz applications.account({root:
root: co.Map<{ draft: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>
AccountRoot,
const AccountRoot: co.Map<{ draft: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>
profile:
profile: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
import co
co.map({
map<{ name: z.z.ZodString; }>(shape: { name: z.z.ZodString; }): co.Map<{ name: z.z.ZodString; }, unknown, Account | Group> export map
name: z.z.ZodString
name:import z
z.string() }), }).
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export string
withMigration((
AccountSchema<{ root: CoMapSchema<{ draft: CoMapSchema<{ name: ZodOptional<ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>; profile: CoMapSchema<...>; }>.withMigration(migration: (account: { readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account, creationProps?: { name: string; }) => void): co.Account<...>
account,
account: { readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account
creationProps?: {
creationProps: { name: string; } | undefined
name: string
name: string }) => { if (!account.
account: { readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account
Account.$jazz: AccountJazzApi<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account>
Jazz methods for Accounts are inside this property. This allows Accounts to be used as plain objects while still having access to Jazz methods.$jazz.AccountJazzApi<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account>.has(key: "root" | "profile"): boolean
has("root")) {account.
account: { readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account
Account.$jazz: AccountJazzApi<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account>
Jazz methods for Accounts are inside this property. This allows Accounts to be used as plain objects while still having access to Jazz methods.$jazz.
AccountJazzApi<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account>.set<"root">(key: "root", value: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | CoMapInit<{ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap>): void
Set the value of a key in the account.set("root", {draft: {}
draft: {} }); } });
Let's not forget to update the AccountSchema
.
import {
function JazzReactProvider<S extends (AccountClass<Account> & CoValueFromRaw<Account>) | CoreAccountSchema>({ children, guestMode, sync, storage, AccountSchema, defaultProfileName, onLogOut, logOutReplacement, onAnonymousAccountDiscarded, enableSSR, fallback, }: JazzProviderProps<S>): JSX.Element
JazzReactProvider } from "jazz-tools/react"; import {JazzAccount } from "./schema"; export function
const JazzAccount: AccountSchema<{ root: CoMapSchema<{ draft: CoMapSchema<{ name: ZodOptional<ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>; profile: CoMapSchema<...>; }>
MyJazzProvider({
function MyJazzProvider({ children }: { children: React.ReactNode; }): React.JSX.Element
children: React.ReactNode
children }: {children: React.ReactNode
children: React.type React.ReactNode = string | number | bigint | boolean | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | Promise<...> | null | undefined
Represents all of the things React can render. Where {@link ReactElement } only represents JSX, `ReactNode` represents everything that can be rendered.ReactNode }) { return ( <function JazzReactProvider<S extends (AccountClass<Account> & CoValueFromRaw<Account>) | CoreAccountSchema>({ children, guestMode, sync, storage, AccountSchema, defaultProfileName, onLogOut, logOutReplacement, onAnonymousAccountDiscarded, enableSSR, fallback, }: JazzProviderProps<S>): JSX.Element
JazzReactProvidersync: SyncConfig
sync={{peer: "wss://cloud.jazz.tools/?key=you@example.com"
peer: "wss://cloud.jazz.tools/?key=you@example.com" }}AccountSchema={
AccountSchema?: AccountSchema<{ root: CoMapSchema<{ draft: CoMapSchema<{ name: ZodOptional<ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>; profile: CoMapSchema<...>; }> | undefined
JazzAccount} > {
const JazzAccount: AccountSchema<{ root: CoMapSchema<{ draft: CoMapSchema<{ name: ZodOptional<ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>; profile: CoMapSchema<...>; }>
children: React.ReactNode
children} </function JazzReactProvider<S extends (AccountClass<Account> & CoValueFromRaw<Account>) | CoreAccountSchema>({ children, guestMode, sync, storage, AccountSchema, defaultProfileName, onLogOut, logOutReplacement, onAnonymousAccountDiscarded, enableSSR, fallback, }: JazzProviderProps<S>): JSX.Element
JazzReactProvider> ); }
Instead of creating a new draft every time we use the create form, let's use the draft from the account root.
// CreateOrder.tsx export function
function CreateOrder(): React.JSX.Element | undefined
CreateOrder() { const {me } =
const me: CoMapLikeLoaded<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account, { ...; }, 10, []> | null | undefined
useAccount<co.Account<{ root: co.Map<{ draft: co.Map<{ name: ZodOptional<ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>; profile: co.Map<...>; }>, { ...; }>(AccountSchema?: co.Account<...> | undefined, options?: { ...; } | undefined): { ...; }
React hook for accessing the current user's account and authentication state. This hook provides access to the current user's account profile and root data, along with authentication utilities. It automatically handles subscription to the user's account data and provides a logout function.useAccount(JazzAccount, {
const JazzAccount: co.Account<{ root: co.Map<{ draft: co.Map<{ name: ZodOptional<ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>; profile: co.Map<...>; }>
resolve?: RefsToResolve<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account, 10, []> | undefined
Resolve query to specify which nested CoValues to load from the accountresolve: {root: {
root?: RefsToResolve<{ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap, 10, [0]> | undefined
draft: true } }, }); if (!
draft?: RefsToResolve<{ readonly name: string | undefined; } & CoMap, 10, [0, 0]> | undefined
me?.
const me: CoMapLikeLoaded<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account, { ...; }, 10, []> | null | undefined
root) return; const
Account.root: ({ readonly draft: { readonly name: string | undefined; } & CoMap; } & { readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | undefined
const onSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave = (e: React.FormEvent<HTMLFormElement>
e: React.interface React.FormEvent<T = Element>
FormEvent<HTMLFormElement>) => {e: React.FormEvent<HTMLFormElement>
e.React.BaseSyntheticEvent<Event, EventTarget & HTMLFormElement, EventTarget>.preventDefault(): void
preventDefault(); constdraft =
const draft: { readonly name: string | undefined; } & CoMap
me.
const me: CoMapLikeLoaded<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account, { ...; }, 10, []>
root.
Account.root: { readonly draft: { readonly name: string | undefined; } & CoMap; } & { readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap
draft; if (!
draft: { readonly name: string | undefined; } & CoMap
draft) return; const
const draft: { readonly name: string | undefined; } & CoMap
validation =
const validation: { errors: string[]; }
validateDraftOrder(
function validateDraftOrder(draft: DraftBubbleTeaOrder): { errors: string[]; }
draft); if (
const draft: { readonly name: string | undefined; } & CoMap
validation.
const validation: { errors: string[]; }
errors: string[]
errors.Array<T>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length > 0) {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 (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(validation.
const validation: { errors: string[]; }
errors: string[]
errors); return; } constorder =
const order: { readonly name: string; } & CoMap
draft as
const draft: { readonly name: string | undefined; } & CoMap
BubbleTeaOrder;
type BubbleTeaOrder = { readonly name: string; } & CoMap
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 (+2 overloads)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log("Order created:",order); // create a new empty draft
const order: { readonly name: string; } & CoMap
me.
const me: CoMapLikeLoaded<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account, { ...; }, 10, []>
root.
Account.root: { readonly draft: { readonly name: string | undefined; } & CoMap; } & { readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap
CoMap.$jazz: CoMapJazzApi<{ readonly draft: { readonly name: string | undefined; } & CoMap; } & { readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap>
Jazz methods for CoMaps are inside this property. This allows CoMaps to be used as plain objects while still having access to Jazz methods, and also doesn't limit which key names can be used inside CoMaps.$jazz.
CoMapJazzApi<{ readonly draft: { readonly name: string | undefined; } & CoMap; } & { readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap>.set<"draft">(key: "draft", value: ({ readonly name: string | undefined; } & CoMap) | CoMapInit<{ readonly name: string | undefined; } & CoMap>): void
Set a value on the CoMapset("draft", {}); }; return <CreateOrderForm
function CreateOrderForm({ id, onSave, }: { id: string; onSave: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element | undefined
id: string
id={me.
const me: CoMapLikeLoaded<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account, { ...; }, 10, []>
root.
Account.root: { readonly draft: { readonly name: string | undefined; } & CoMap; } & { readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap
draft.
draft: { readonly name: string | undefined; } & CoMap
CoMap.$jazz: CoMapJazzApi<{ readonly name: string | undefined; } & CoMap>
Jazz methods for CoMaps are inside this property. This allows CoMaps to be used as plain objects while still having access to Jazz methods, and also doesn't limit which key names can be used inside CoMaps.$jazz.CoMapJazzApi<M extends CoMap>.id: string
The ID of this `CoMap`id}onSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave={const onSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave} /> } functionCreateOrderForm({
function CreateOrderForm({ id, onSave, }: { id: string; onSave: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element | undefined
id: string
id,onSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave, }: {id: string
id: stringonSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave: (e: React.FormEvent<HTMLFormElement>
e: React.interface React.FormEvent<T = Element>
FormEvent<HTMLFormElement>) => void; }) { constdraft =
const draft: ({ readonly name: string | undefined; } & CoMap) | null | undefined
useCoState<co.Map<{ name: ZodOptional<ZodString>; }, unknown, Account | Group>, true>(Schema: co.Map<{ name: ZodOptional<ZodString>; }, unknown, Account | Group>, id: string | undefined, options?: { ...; } | undefined): ({ ...; } & CoMap) | ... 1 more ... | undefined
React hook for subscribing to CoValues and handling loading states. This hook provides a convenient way to subscribe to CoValues and automatically handles the subscription lifecycle (subscribe on mount, unsubscribe on unmount). It also supports deep loading of nested CoValues through resolve queries.useCoState(DraftBubbleTeaOrder,
const DraftBubbleTeaOrder: co.Map<{ name: ZodOptional<ZodString>; }, unknown, Account | Group>
id: string
id); if (!draft) return; return <
const draft: ({ readonly name: string | undefined; } & CoMap) | null | undefined
OrderForm
function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | DraftBubbleTeaOrder; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element
order={
order: ({ readonly name: string | undefined; } & CoMap) | ({ readonly name: string; } & CoMap)
draft}
const draft: { readonly name: string | undefined; } & CoMap
onSave?: ((e: React.FormEvent<HTMLFormElement>) => void) | undefined
onSave={onSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave} />; }
When the new draft is created, we need to call useCoState
again, so that we are passing the new draft to <OrderForm/>
.
There you have it! Notice that when you refresh the page, you will see your unsaved changes.
Draft indicator
To improve the UX even further, in just a few more steps, we can tell the user that they currently have unsaved changes.
Simply add a hasChanges
helper to your schema.
// schema.ts export const
BubbleTeaOrder =
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
import co
co.map({
map<{ name: z.z.ZodString; }>(shape: { name: z.z.ZodString; }): co.Map<{ name: z.z.ZodString; }, unknown, Account | Group> export map
name: z.z.ZodString
name:import z
z.string(), }); export type
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export string
BubbleTeaOrder =
type BubbleTeaOrder = { readonly name: string; } & CoMap
import co
co.loaded<typeof
type loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loaded
BubbleTeaOrder>; export const
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
DraftBubbleTeaOrder =
const DraftBubbleTeaOrder: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
BubbleTeaOrder.
const BubbleTeaOrder: co.Map<{ name: z.z.ZodString; }, unknown, Account | Group>
CoMapSchema<{ name: ZodString; }, unknown, Account | Group>.partial(): co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
Creates a new CoMap schema by making all fields optional.partial(); export typeDraftBubbleTeaOrder =
type DraftBubbleTeaOrder = { readonly name: string | undefined; } & CoMap
import co
co.loaded<typeof
type loaded<T extends CoValueClassOrSchema, R extends ResolveQuery<T> = true> = R extends boolean | undefined ? NonNullable<InstanceOfSchemaCoValuesNullable<T>> : [NonNullable<InstanceOfSchemaCoValuesNullable<T>>] extends [...] ? Exclude<...> extends CoValue ? R extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean | undefined ? CoValue & Exclude<...> : [...] extends [...] ? Exclude<...> extends CoValue ? ItemDepth extends { ...; } ? readonly ((CoValue & ... 1 more ... & (ItemDepth extends boolean ... export loaded
DraftBubbleTeaOrder>; export function
const DraftBubbleTeaOrder: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>
validateDraftOrder(
function validateDraftOrder(draft: DraftBubbleTeaOrder): { errors: string[]; }
draft:
draft: { readonly name: string | undefined; } & CoMap
DraftBubbleTeaOrder) { const
type DraftBubbleTeaOrder = { readonly name: string | undefined; } & CoMap
const errors: string[]
errors: string[] = []; if (!draft.
draft: { readonly name: string | undefined; } & CoMap
name: string | undefined
name) {const errors: string[]
errors.Array<string>.push(...items: string[]): number
Appends new elements to the end of an array, and returns the new length of the array.push("Please enter a name."); } return {errors: string[]
errors }; }; export functionfunction hasChanges(draft?: DraftBubbleTeaOrder): number | false
hasChanges(draft?:
draft: ({ readonly name: string | undefined; } & CoMap) | undefined
DraftBubbleTeaOrder) { return
type DraftBubbleTeaOrder = { readonly name: string | undefined; } & CoMap
draft ?
draft: ({ readonly name: string | undefined; } & CoMap) | undefined
var Object: ObjectConstructor
Provides functionality common to all JavaScript objects.Object.ObjectConstructor.keys(o: {}): string[] (+1 overload)
Returns the names of the enumerable string properties and methods of an object.keys(draft.
draft: { readonly name: string | undefined; } & CoMap
CoMap.$jazz: CoMapJazzApi<{ readonly name: string | undefined; } & CoMap>
Jazz methods for CoMaps are inside this property. This allows CoMaps to be used as plain objects while still having access to Jazz methods, and also doesn't limit which key names can be used inside CoMaps.$jazz.
CoMapJazzApi<{ readonly name: string | undefined; } & CoMap>.getEdits(): CoMapEdits<{ readonly name: string | undefined; } & CoMap>
Get the edits made to the CoMap.getEdits()).Array<T>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length : false; };
In the UI, you can choose how you want to show the draft indicator.
// DraftIndicator.tsx export function
function DraftIndicator(): React.JSX.Element | undefined
DraftIndicator() { const {me } =
const me: CoMapLikeLoaded<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account, { ...; }, 10, []> | null | undefined
useAccount<co.Account<{ root: co.Map<{ draft: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>; profile: co.Map<...>; }>, { ...; }>(AccountSchema?: co.Account<...> | undefined, options?: { ...; } | undefined): { ...; }
React hook for accessing the current user's account and authentication state. This hook provides access to the current user's account profile and root data, along with authentication utilities. It automatically handles subscription to the user's account data and provides a logout function.useAccount(JazzAccount, {
const JazzAccount: co.Account<{ root: co.Map<{ draft: co.Map<{ name: z.ZodOptional<z.z.ZodString>; }, unknown, Account | Group>; }, unknown, Account | Group>; profile: co.Map<{ ...; }, unknown, Account | Group>; }>
resolve?: RefsToResolve<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account, 10, []> | undefined
Resolve query to specify which nested CoValues to load from the accountresolve: {root: {
root?: RefsToResolve<{ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap, 10, [0]> | undefined
draft: true } }, }); if (
draft?: RefsToResolve<{ readonly name: string | undefined; } & CoMap, 10, [0, 0]> | undefined
function hasChanges(draft?: DraftBubbleTeaOrder): number | false
hasChanges(me?.
const me: CoMapLikeLoaded<{ readonly root: ({ readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap) | null; readonly profile: ({ readonly name: string; } & CoMap) | null; } & Account, { ...; }, 10, []> | null | undefined
root.
Account.root: { readonly draft: { readonly name: string | undefined; } & CoMap; } & { readonly draft: ({ readonly name: string | undefined; } & CoMap) | null; } & CoMap
draft)) { return ( <
draft: ({ readonly name: string | undefined; } & CoMap) | undefined
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p>You have a draft</React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p> ); } }
A more subtle way is to show a small dot next to the Create button.
Handling different types of data
Forms can be more complex than just a single string field, so we've put together an example app that shows you how to handle single-select, multi-select, date, and boolean inputs.
// schema.ts export const
BubbleTeaOrder =
const BubbleTeaOrder: co.Map<{ baseTea: z.z.ZodLiteral<"Black" | "Oolong" | "Jasmine" | "Thai">; addOns: co.List<z.z.ZodLiteral<"Pearl" | "Lychee jelly" | "Red bean" | "Brown sugar" | "Taro">>; deliveryDate: z.z.ZodDate; withMilk: z.z.ZodBoolean; instructions: z.ZodOptional<...>; }, unknown, Account | Group>
import co
co.map({
map<{ baseTea: z.z.ZodLiteral<"Black" | "Oolong" | "Jasmine" | "Thai">; addOns: co.List<z.z.ZodLiteral<"Pearl" | "Lychee jelly" | "Red bean" | "Brown sugar" | "Taro">>; deliveryDate: z.z.ZodDate; withMilk: z.z.ZodBoolean; instructions: z.ZodOptional<...>; }>(shape: { ...; }): co.Map<...> export map
baseTea: z.z.ZodLiteral<"Black" | "Oolong" | "Jasmine" | "Thai">
baseTea:import z
z.literal(["Black", "Oolong", "Jasmine", "Thai"]),
literal<readonly ["Black", "Oolong", "Jasmine", "Thai"]>(value: readonly ["Black", "Oolong", "Jasmine", "Thai"], params?: string | z.z.core.$ZodLiteralParams): z.z.ZodLiteral<"Black" | "Oolong" | "Jasmine" | "Thai"> (+1 overload) export literal
addOns: co.List<z.z.ZodLiteral<"Pearl" | "Lychee jelly" | "Red bean" | "Brown sugar" | "Taro">>
addOns:const ListOfBubbleTeaAddOns: co.List<z.z.ZodLiteral<"Pearl" | "Lychee jelly" | "Red bean" | "Brown sugar" | "Taro">>
ListOfBubbleTeaAddOns,deliveryDate: z.z.ZodDate
deliveryDate:import z
z.date(),
function date(params?: string | z.z.core.$ZodDateParams): z.z.ZodDate export date
withMilk: z.z.ZodBoolean
withMilk:import z
z.boolean(),
function boolean(params?: string | z.z.core.$ZodBooleanParams): z.z.ZodBoolean export boolean
instructions: z.ZodOptional<z.z.ZodString>
instructions:import z
z.optional(
optional<z.z.ZodString>(innerType: z.z.ZodString): z.ZodOptional<z.z.ZodString> export optional
import z
z.string()), });
function string(params?: string | z.z.core.$ZodStringParams): z.z.ZodString (+1 overload) export string