Creating and updating CoValues in a form
Normally, we implement forms using the onSubmit handler, or by making a controlled form with useState, or by using special libraries like react-hook-form.
In Jazz, we can do something simpler and more powerful, because CoValues give us reactive, persisted state which we can use to directly edit live objects, and represent auto-saved drafts.
Updating a CoValue
To update a CoValue, we simply assign the new value directly as changes happen. These changes are synced to the server, so we don't need to handle form submissions either.
<input type="text" value={order.name} onChange={(e) => order.name = e.target.value} />
This means we can write update forms in fewer lines of code.
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 class
class BubbleTeaOrder
BubbleTeaOrder extendsclass CoMap
CoMaps are collaborative versions of plain objects, mapping string-like keys to values.CoMap {BubbleTeaOrder.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; } export classclass DraftBubbleTeaOrder
DraftBubbleTeaOrder extendsclass CoMap
CoMaps are collaborative versions of plain objects, mapping string-like keys to values.CoMap {DraftBubbleTeaOrder.name: co<string | undefined>
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>; }; }
optional.
optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; ... 7 more ...; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }
string: co<string | undefined>
string; }
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: BubbleTeaOrder | DraftBubbleTeaOrder
order,onSave: ((e: React.FormEvent<HTMLFormElement>) => void) | undefined
onSave, }: {order: BubbleTeaOrder | DraftBubbleTeaOrder
order:class BubbleTeaOrder
BubbleTeaOrder |class DraftBubbleTeaOrder
DraftBubbleTeaOrder;onSave?: ((e: React.FormEvent<HTMLFormElement>) => void) | undefined
onSave?: (e: React.FormEvent<HTMLFormElement>
e: React.interface React.FormEvent<T = Element>
FormEvent<HTMLFormElement>) => void; }) { return ( <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}> <JSX.IntrinsicElements.label: React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>
label> Name <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: BubbleTeaOrder | DraftBubbleTeaOrder
order.name: string | (string & CoMarker) | undefined
name}React.InputHTMLAttributes<HTMLInputElement>.onChange?: React.ChangeEventHandler<HTMLInputElement> | undefined
onChange={(e: React.ChangeEvent<HTMLInputElement>
e) => (order: BubbleTeaOrder | DraftBubbleTeaOrder
order.name: string | (string & CoMarker) | undefined
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. [MDN Reference](https://developer.mozilla.org/docs/Web/API/HTMLInputElement/value)value)}React.InputHTMLAttributes<HTMLInputElement>.required?: boolean | undefined
required /> </JSX.IntrinsicElements.label: React.DetailedHTMLProps<React.LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>
label> {onSave: ((e: React.FormEvent<HTMLFormElement>) => void) | undefined
onSave && <JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
buttonReact.ButtonHTMLAttributes<HTMLButtonElement>.type?: "button" | "reset" | "submit" | undefined
type="submit">Submit</JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button>} </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: ID<BubbleTeaOrder>; }): React.JSX.Element | undefined
props: {
props: { id: ID<BubbleTeaOrder>; }
id: ID<BubbleTeaOrder>
id:type ID<T> = `co_z${string}` & IDMarker<T>
IDs are unique identifiers for `CoValue`s. Can be used with a type argument to refer to a specific `CoValue` type.ID<class BubbleTeaOrder
BubbleTeaOrder> }) { constconst order: BubbleTeaOrder | null | undefined
order =useCoState(
useCoState<BubbleTeaOrder, true>(Schema: CoValueClass<BubbleTeaOrder>, id: ID<CoValue> | undefined, options?: { resolve?: RefsToResolve<...> | undefined; } | undefined): BubbleTeaOrder | ... 1 more ... | undefined
class BubbleTeaOrder
BubbleTeaOrder,props.
props: { id: ID<BubbleTeaOrder>; }
id: ID<BubbleTeaOrder>
id); if (!const order: BubbleTeaOrder | null | undefined
order) return; return <OrderForm
function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | DraftBubbleTeaOrder; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element
order: BubbleTeaOrder | DraftBubbleTeaOrder
order={const order: BubbleTeaOrder
order} />; }
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 {const me: JazzAccount
me } =useAccount(); const [
useAccount<JazzAccount>(): { me: JazzAccount; logOut: () => void; } (+1 overload)
const draft: DraftBubbleTeaOrder | undefined
draft,const setDraft: React.Dispatch<React.SetStateAction<DraftBubbleTeaOrder | undefined>>
setDraft] =useState<DraftBubbleTeaOrder>(): [DraftBubbleTeaOrder | undefined, React.Dispatch<React.SetStateAction<DraftBubbleTeaOrder | undefined>>] (+1 overload)
Returns a stateful value, and a function to update it.useState<class DraftBubbleTeaOrder
DraftBubbleTeaOrder>();function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void
Accepts a function that contains imperative, possibly effectful code.useEffect(() => {const setDraft: (value: React.SetStateAction<DraftBubbleTeaOrder | undefined>) => void
setDraft(class DraftBubbleTeaOrder
DraftBubbleTeaOrder.
CoMap.create<DraftBubbleTeaOrder>(this: CoValueClass<...>, init: { name?: string | (string & CoMarker) | null | undefined; }, options?: { owner: Account | Group; unique?: CoValueUniqueness["uniqueness"]; } | Account | Group): DraftBubbleTeaOrder
Create a new CoMap with the given initial values and owner. The owner (a Group or Account) determines access rights to the CoMap. The CoMap will immediately be persisted and synced to connected peers.create({})); }, [const me: JazzAccount
me?.Account.id: ID<JazzAccount>
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 (!const draft: DraftBubbleTeaOrder | undefined
draft) return; constconst order: BubbleTeaOrder
order =const draft: DraftBubbleTeaOrder
draft asclass BubbleTeaOrder
BubbleTeaOrder;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("Order created:",const order: BubbleTeaOrder
order); }; if (!const draft: DraftBubbleTeaOrder | undefined
draft) return; return <OrderForm
function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | DraftBubbleTeaOrder; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element
order: DraftBubbleTeaOrder | BubbleTeaOrder
order={const draft: DraftBubbleTeaOrder
draft}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 validate
method.
// schema.ts export class
class DraftBubbleTeaOrder
DraftBubbleTeaOrder extendsclass CoMap
CoMaps are collaborative versions of plain objects, mapping string-like keys to values.CoMap {DraftBubbleTeaOrder.name: co<string | undefined>
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>; }; }
optional.
optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; ... 7 more ...; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }
string: co<string | undefined>
string;validate() { const
DraftBubbleTeaOrder.validate(): { errors: string[]; }
const errors: string[]
errors: string[] = []; if (!this.DraftBubbleTeaOrder.name: co<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 {const me: JazzAccount
me } =useAccount(); const [
useAccount<JazzAccount>(): { me: JazzAccount; logOut: () => void; } (+1 overload)
const draft: DraftBubbleTeaOrder | undefined
draft,const setDraft: React.Dispatch<React.SetStateAction<DraftBubbleTeaOrder | undefined>>
setDraft] =useState<DraftBubbleTeaOrder>(): [DraftBubbleTeaOrder | undefined, React.Dispatch<React.SetStateAction<DraftBubbleTeaOrder | undefined>>] (+1 overload)
Returns a stateful value, and a function to update it.useState<class DraftBubbleTeaOrder
DraftBubbleTeaOrder>();function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void
Accepts a function that contains imperative, possibly effectful code.useEffect(() => {const setDraft: (value: React.SetStateAction<DraftBubbleTeaOrder | undefined>) => void
setDraft(class DraftBubbleTeaOrder
DraftBubbleTeaOrder.
CoMap.create<DraftBubbleTeaOrder>(this: CoValueClass<...>, init: { name?: string | (string & CoMarker) | null | undefined; }, options?: { owner: Account | Group; unique?: CoValueUniqueness["uniqueness"]; } | Account | Group): DraftBubbleTeaOrder
Create a new CoMap with the given initial values and owner. The owner (a Group or Account) determines access rights to the CoMap. The CoMap will immediately be persisted and synced to connected peers.create({})); }, [const me: JazzAccount
me?.Account.id: ID<JazzAccount>
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 (!const draft: DraftBubbleTeaOrder | undefined
draft) return; constvalidation =
const validation: { errors: string[]; }
const draft: DraftBubbleTeaOrder
draft.validate(); if (
DraftBubbleTeaOrder.validate(): { errors: string[]; }
validation.
const validation: { errors: string[]; }
errors: string[]
errors.Array<string>.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 (+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(validation.
const validation: { errors: string[]; }
errors: string[]
errors); return; } constconst order: BubbleTeaOrder
order =const draft: DraftBubbleTeaOrder
draft asclass BubbleTeaOrder
BubbleTeaOrder;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("Order created:",const order: BubbleTeaOrder
order); }; if (!const draft: DraftBubbleTeaOrder | undefined
draft) return; return <OrderForm
function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | DraftBubbleTeaOrder; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element
order: DraftBubbleTeaOrder | BubbleTeaOrder
order={const draft: DraftBubbleTeaOrder
draft}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 class
class BubbleTeaOrder
BubbleTeaOrder extendsclass CoMap
CoMaps are collaborative versions of plain objects, mapping string-like keys to values.CoMap {BubbleTeaOrder.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; } export classclass DraftBubbleTeaOrder
DraftBubbleTeaOrder extendsclass CoMap
CoMaps are collaborative versions of plain objects, mapping string-like keys to values.CoMap {DraftBubbleTeaOrder.name: co<string | undefined>
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>; }; }
optional.
optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; ... 7 more ...; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }
string: co<string | undefined>
string; } export classclass AccountRoot
AccountRoot extendsclass CoMap
CoMaps are collaborative versions of plain objects, mapping string-like keys to values.CoMap {AccountRoot.draft: co<DraftBubbleTeaOrder | null>
draft =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(
ref: <typeof DraftBubbleTeaOrder>(arg: typeof DraftBubbleTeaOrder | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof DraftBubbleTeaOrder), options?: never) => co<...> (+1 overload)
class DraftBubbleTeaOrder
DraftBubbleTeaOrder); } export classclass JazzAccount
JazzAccount extendsclass Account
Account {JazzAccount.root: co<AccountRoot | null>
root =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(
ref: <typeof AccountRoot>(arg: typeof AccountRoot | ((_raw: RawCoMap<{ [key: string]: JsonValue | undefined; }, JsonObject | null>) => typeof AccountRoot), options?: never) => co<...> (+1 overload)
class AccountRoot
AccountRoot);migrate(
JazzAccount.migrate(this: JazzAccount, creationProps?: { name: string; }): void
this: JazzAccount
this:class JazzAccount
JazzAccount,creationProps?: {
creationProps: { name: string; } | undefined
name: string
name: string }) { if (this.JazzAccount.root: co<AccountRoot | null>
root ===var undefined
undefined) { constconst draft: DraftBubbleTeaOrder
draft =class DraftBubbleTeaOrder
DraftBubbleTeaOrder.
CoMap.create<DraftBubbleTeaOrder>(this: CoValueClass<...>, init: { name?: string | (string & CoMarker) | null | undefined; }, options?: { owner: Account | Group; unique?: CoValueUniqueness["uniqueness"]; } | Account | Group): DraftBubbleTeaOrder
Create a new CoMap with the given initial values and owner. The owner (a Group or Account) determines access rights to the CoMap. The CoMap will immediately be persisted and synced to connected peers.create({}); this.JazzAccount.root: co<AccountRoot | null>
root =class AccountRoot
AccountRoot.
CoMap.create<AccountRoot>(this: CoValueClass<...>, init: { draft: (DraftBubbleTeaOrder | NonNullable<DraftBubbleTeaOrder & CoMarker>) & (DraftBubbleTeaOrder | ... 1 more ... | undefined); }, options?: { owner: Account | Group; unique?: CoValueUniqueness["uniqueness"]; } | Account | Group): AccountRoot
Create a new CoMap with the given initial values and owner. The owner (a Group or Account) determines access rights to the CoMap. The CoMap will immediately be persisted and synced to connected peers.create({draft: (DraftBubbleTeaOrder | NonNullable<DraftBubbleTeaOrder & CoMarker>) & (DraftBubbleTeaOrder | NonNullable<...> | undefined)
draft }); } } }
Let's not forget to update the AccountSchema
.
import {
function JazzProvider<Acc extends Account = JazzAccount>({ children, guestMode, sync, storage, AccountSchema, defaultProfileName, onLogOut, logOutReplacement, onAnonymousAccountDiscarded, }: JazzProviderProps<Acc>): JSX.Element
JazzProvider } from "jazz-react"; import {class JazzAccount
JazzAccount } from "./schema"; export functionMyJazzProvider({
function MyJazzProvider({ children }: { children: React.ReactNode; }): React.JSX.Element
children: React.ReactNode
children }: {children: React.ReactNode
children: React.type React.ReactNode = string | number | boolean | React.ReactElement<any, string | React.JSXElementConstructor<any>> | Iterable<React.ReactNode> | React.ReactPortal | 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 JazzProvider<Acc extends Account = JazzAccount>({ children, guestMode, sync, storage, AccountSchema, defaultProfileName, onLogOut, logOutReplacement, onAnonymousAccountDiscarded, }: JazzProviderProps<Acc>): JSX.Element
JazzProvidersync: SyncConfig
sync={{peer: "wss://cloud.jazz.tools/?key=you@example.com"
peer: "wss://cloud.jazz.tools/?key=you@example.com" }}AccountSchema?: AccountClass<JazzAccount> | undefined
AccountSchema={class JazzAccount
JazzAccount} > {children: React.ReactNode
children} </function JazzProvider<Acc extends Account = JazzAccount>({ children, guestMode, sync, storage, AccountSchema, defaultProfileName, onLogOut, logOutReplacement, onAnonymousAccountDiscarded, }: JazzProviderProps<Acc>): JSX.Element
JazzProvider> ); } // Register the Account schema so `useAccount` returns our custom `JazzAccount` declare module "jazz-react" { interface Register {Register.Account: JazzAccount
Account:class JazzAccount
JazzAccount; } }
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: ({ root: { draft: DraftBubbleTeaOrder; } & AccountRoot; } & JazzAccount) | null | undefined
useAccount({
useAccount<JazzAccount, { root: { draft: true; }; }>(options?: { resolve?: RefsToResolve<JazzAccount, 10, []> | undefined; } | undefined): { me: ({ root: { ...; } & AccountRoot; } & JazzAccount) | null | undefined; logOut: () => void; } (+1 overload)
resolve?: RefsToResolve<JazzAccount, 10, []> | undefined
resolve: {root?: RefsToResolve<AccountRoot, 10, [0]> | undefined
root: {draft?: RefsToResolve<DraftBubbleTeaOrder, 10, [0, 0]> | undefined
draft: true } }, }); if (!me?.
const me: ({ root: { draft: DraftBubbleTeaOrder; } & AccountRoot; } & JazzAccount) | null | undefined
root) return; const
JazzAccount.root: ({ draft: DraftBubbleTeaOrder; } & AccountRoot & co<AccountRoot | null>) | 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(); constconst draft: DraftBubbleTeaOrder & co<DraftBubbleTeaOrder | null>
draft =me.
const me: { root: { draft: DraftBubbleTeaOrder; } & AccountRoot; } & JazzAccount
root.
JazzAccount.root: { draft: DraftBubbleTeaOrder; } & AccountRoot & co<AccountRoot | null>
AccountRoot.draft: DraftBubbleTeaOrder & co<DraftBubbleTeaOrder | null>
draft; if (!const draft: DraftBubbleTeaOrder & co<DraftBubbleTeaOrder | null>
draft) return; constvalidation =
const validation: { errors: string[]; }
const draft: DraftBubbleTeaOrder & co<DraftBubbleTeaOrder | null>
draft.validate(); if (
DraftBubbleTeaOrder.validate(): { errors: string[]; }
validation.
const validation: { errors: string[]; }
errors: string[]
errors.Array<string>.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 (+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(validation.
const validation: { errors: string[]; }
errors: string[]
errors); return; } constconst order: BubbleTeaOrder
order =const draft: DraftBubbleTeaOrder & co<DraftBubbleTeaOrder | null>
draft asclass BubbleTeaOrder
BubbleTeaOrder;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("Order created:",const order: BubbleTeaOrder
order); // create a new empty draftme.
const me: { root: { draft: DraftBubbleTeaOrder; } & AccountRoot; } & JazzAccount
root.
JazzAccount.root: { draft: DraftBubbleTeaOrder; } & AccountRoot & co<AccountRoot | null>
AccountRoot.draft: DraftBubbleTeaOrder & co<DraftBubbleTeaOrder | null>
draft =class DraftBubbleTeaOrder
DraftBubbleTeaOrder.
CoMap.create<DraftBubbleTeaOrder>(this: CoValueClass<...>, init: { name?: string | (string & CoMarker) | null | undefined; }, options?: { owner: Account | Group; unique?: CoValueUniqueness["uniqueness"]; } | Account | Group): DraftBubbleTeaOrder
Create a new CoMap with the given initial values and owner. The owner (a Group or Account) determines access rights to the CoMap. The CoMap will immediately be persisted and synced to connected peers.create( {}, ); }; return <CreateOrderForm
function CreateOrderForm({ id, onSave, }: { id: ID<DraftBubbleTeaOrder>; onSave: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element | undefined
id: ID<DraftBubbleTeaOrder>
id={me.
const me: { root: { draft: DraftBubbleTeaOrder; } & AccountRoot; } & JazzAccount
root.
JazzAccount.root: { draft: DraftBubbleTeaOrder; } & AccountRoot & co<AccountRoot | null>
AccountRoot.draft: DraftBubbleTeaOrder & co<DraftBubbleTeaOrder | null>
draft.CoMap.id: ID<DraftBubbleTeaOrder> | ID<DraftBubbleTeaOrder & CoMarker>
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: ID<DraftBubbleTeaOrder>; onSave: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element | undefined
id: ID<DraftBubbleTeaOrder>
id,onSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave, }: {id: ID<DraftBubbleTeaOrder>
id:type ID<T> = `co_z${string}` & IDMarker<T>
IDs are unique identifiers for `CoValue`s. Can be used with a type argument to refer to a specific `CoValue` type.ID<class DraftBubbleTeaOrder
DraftBubbleTeaOrder>;onSave: (e: React.FormEvent<HTMLFormElement>) => void
onSave: (e: React.FormEvent<HTMLFormElement>
e: React.interface React.FormEvent<T = Element>
FormEvent<HTMLFormElement>) => void; }) { constconst draft: DraftBubbleTeaOrder | null | undefined
draft =useCoState(
useCoState<DraftBubbleTeaOrder, true>(Schema: CoValueClass<DraftBubbleTeaOrder>, id: ID<CoValue> | undefined, options?: { ...; } | undefined): DraftBubbleTeaOrder | ... 1 more ... | undefined
class DraftBubbleTeaOrder
DraftBubbleTeaOrder,id: ID<DraftBubbleTeaOrder>
id); if (!const draft: DraftBubbleTeaOrder | null | undefined
draft) return; return <OrderForm
function OrderForm({ order, onSave, }: { order: BubbleTeaOrder | DraftBubbleTeaOrder; onSave?: (e: React.FormEvent<HTMLFormElement>) => void; }): React.JSX.Element
order: DraftBubbleTeaOrder | BubbleTeaOrder
order={const draft: DraftBubbleTeaOrder
draft}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
checker to your schema.
// schema.ts export class
class DraftBubbleTeaOrder
DraftBubbleTeaOrder extendsclass CoMap
CoMaps are collaborative versions of plain objects, mapping string-like keys to values.CoMap {DraftBubbleTeaOrder.name: co<string | undefined>
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>; }; }
optional.
optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; ... 7 more ...; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }
string: co<string | undefined>
string;validate() { const
DraftBubbleTeaOrder.validate(): { errors: string[]; }
const errors: string[]
errors: string[] = []; if (!this.DraftBubbleTeaOrder.name: co<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("Plese enter a name."); } return {errors: string[]
errors }; } getDraftBubbleTeaOrder.hasChanges: number
hasChanges() { returnvar 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(this.CoMap._edits: { [Key in Exclude<keyof this & string, keyof CoMap>]: IfCo<this[Key], LastAndAllCoMapEdits<this[Key]>>; }
_edits).Array<string>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length; } }
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: ({ root: { draft: DraftBubbleTeaOrder; } & AccountRoot; } & JazzAccount) | null | undefined
useAccount({
useAccount<JazzAccount, { root: { draft: true; }; }>(options?: { resolve?: RefsToResolve<JazzAccount, 10, []> | undefined; } | undefined): { me: ({ root: { ...; } & AccountRoot; } & JazzAccount) | null | undefined; logOut: () => void; } (+1 overload)
resolve?: RefsToResolve<JazzAccount, 10, []> | undefined
resolve: {root?: RefsToResolve<AccountRoot, 10, [0]> | undefined
root: {draft?: RefsToResolve<DraftBubbleTeaOrder, 10, [0, 0]> | undefined
draft: true } }, }); if (me?.
const me: ({ root: { draft: DraftBubbleTeaOrder; } & AccountRoot; } & JazzAccount) | null | undefined
root.
JazzAccount.root: { draft: DraftBubbleTeaOrder; } & AccountRoot & co<AccountRoot | null>
AccountRoot.draft: (DraftBubbleTeaOrder & co<DraftBubbleTeaOrder | null>) | undefined
draft?.DraftBubbleTeaOrder.hasChanges: number | undefined
hasChanges) { return ( <JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p>You have a draft</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 class
class BubbleTeaOrder
BubbleTeaOrder extendsclass CoMap
CoMaps are collaborative versions of plain objects, mapping string-like keys to values.CoMap {BubbleTeaOrder.baseTea: co<"Black" | "Oolong" | "Jasmine" | "Thai">
baseTea =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>; }; }
literal<["Black", "Oolong", "Jasmine", "Thai"]>(_lit_0: "Black", _lit_1: "Oolong", _lit_2: "Jasmine", _lit_3: "Thai"): co<"Black" | "Oolong" | "Jasmine" | "Thai">
literal(...const BubbleTeaBaseTeaTypes: readonly ["Black", "Oolong", "Jasmine", "Thai"]
BubbleTeaBaseTeaTypes);BubbleTeaOrder.addOns: co<ListOfBubbleTeaAddOns | null>
addOns =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 ListOfBubbleTeaAddOns>(arg: typeof ListOfBubbleTeaAddOns | ((_raw: RawCoList<JsonValue, null>) => typeof ListOfBubbleTeaAddOns), options?: never) => co<...> (+1 overload)
ref(class ListOfBubbleTeaAddOns
ListOfBubbleTeaAddOns);BubbleTeaOrder.deliveryDate: co<Date>
deliveryDate =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>; }; }
type Date: co<Date>
Date;BubbleTeaOrder.withMilk: co<boolean>
withMilk =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>; }; }
boolean: co<boolean>
boolean;BubbleTeaOrder.instructions: co<string | undefined>
instructions =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>; }; }
optional.
optional: { ref: <C extends CoValueClass>(arg: C | ((_raw: InstanceType<C>["_raw"]) => C)) => co<InstanceType<C> | null | undefined>; ... 7 more ...; literal<T extends (string | number | boolean)[]>(..._lit: T): co<T[number] | undefined>; }
string: co<string | undefined>
string; }