Add snapshot prop, examples (#1856)

This PR:
- adds a `snapshot` prop to the <Tldraw> component. It does basically
the same thing as calling `loadSnapshot` after creating the store, but
happens before the editor actually loads.
- adds a largeish example (including a JSON snapshot) to the examples

We have some very complex ways of juggling serialized data between
multiplayer, file formats, and the snapshot APIs. I'd like to see these
simplified, or at least for our documentation to reflect a narrow subset
of all the options available.

The most common questions seem to be:

Q: How do I serialize data?
A: Via the `Editor.getSnapshot()` method

Q: How do I restore serialized data?
A: Via the `Editor.loadSnapshot()` method OR via the `<Tldraw>`
component's `snapshot` prop

The store has an `initialData` constructor prop, however this is quite
complex as the store also requires a schema class instance with which to
migrate the data. In our components (<Tldraw> and <TldrawEditor>) we
were also accepting `initialData`, however we weren't accepting a
schema, and either way I think it's unrealistic to also expect users to
create schemas themselves and pass those in.

AFAIK the `initialData` prop is only used in the file loading, which is
a good example of how complex it looks like to create a schema and
migrate data outside of the components.

### Change Type

- [x] `minor` — New feature
pull/1848/head
Steve Ruiz 2023-09-08 15:48:55 +01:00 zatwierdzone przez GitHub
rodzic f21eaeb4d8
commit 0b3e83be52
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
11 zmienionych plików z 38344 dodań i 10 usunięć

Wyświetl plik

@ -70,4 +70,9 @@ test.describe('Routes', () => {
await page.goto('http://localhost:5420/persistence')
await page.waitForSelector('.tl-canvas')
})
test('snapshots', async ({ page }) => {
await page.goto('http://localhost:5420/snapshots')
await page.waitForSelector('.tl-canvas')
})
})

Wyświetl plik

@ -0,0 +1,39 @@
import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import jsonSnapshot from './snapshot.json'
// ^^^
// This snapshot was previously created with `editor.store.getSnapshot()`
// We'll now load this into the editor with `editor.store.loadSnapshot()`.
// Loading it also migrates the snapshot, so even though the snapshot was
// created in the past (potentially a few versions ago) it should load
// successfully.
const LOAD_SNAPSHOT_WITH_INITIAL_DATA = true
export default function SnapshotExample() {
if (LOAD_SNAPSHOT_WITH_INITIAL_DATA) {
// If you want to use the snapshot as the store's initial data, you can do so like this:
return (
<div className="tldraw__editor">
<Tldraw snapshot={jsonSnapshot} />
</div>
)
}
// You can also load the snapshot an existing editor instance afterwards. Note that this
// does not create a new editor, and doesn't change the editor's state or the editor's undo
// history, so you should only ever use this on mount.
return (
<div className="tldraw__editor">
<Tldraw
autoFocus
onMount={(editor) => {
editor.store.loadSnapshot(jsonSnapshot)
}}
/>
</div>
)
}
// Tips:
// Want to migrate a snapshot but not load it? Use `editor.store.migrateSnapshot()`

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -27,6 +27,7 @@ import MultipleExample from './examples/MultipleExample'
import PersistenceExample from './examples/PersistenceExample'
import ScrollExample from './examples/ScrollExample'
import ShapeMetaExample from './examples/ShapeMetaExample'
import SnapshotExample from './examples/SnapshotExample/SnapshotExample'
import StoreEventsExample from './examples/StoreEventsExample'
import UiEventsExample from './examples/UiEventsExample'
import UserPresenceExample from './examples/UserPresenceExample'
@ -135,6 +136,11 @@ export const allExamples: Example[] = [
path: '/persistence',
element: <PersistenceExample />,
},
{
title: 'Snapshots',
path: '/snapshots',
element: <SnapshotExample />,
},
{
title: 'Custom styles',
path: '/custom-styles',

Wyświetl plik

@ -1,6 +1,6 @@
{
"extends": "../../config/tsconfig.base.json",
"include": ["src", "e2e", "./vite.config.ts"],
"include": ["src", "e2e", "./vite.config.ts", "**/*.json"],
"exclude": ["node_modules", "dist", "**/*.css", ".tsbuild*", "./scripts/legacy-translations"],
"compilerOptions": {
"outDir": "./.tsbuild"

Wyświetl plik

@ -31,6 +31,7 @@ import { SerializedStore } from '@tldraw/store';
import { ShapeProps } from '@tldraw/tlschema';
import { Signal } from '@tldraw/state';
import { StoreSchema } from '@tldraw/store';
import { StoreSnapshot } from '@tldraw/store';
import { StyleProp } from '@tldraw/tlschema';
import { TLArrowShape } from '@tldraw/tlschema';
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema';
@ -2009,6 +2010,7 @@ export type TldrawEditorProps = TldrawEditorBaseProps & ({
store: TLStore | TLStoreWithStatus;
} | {
store?: undefined;
snapshot?: StoreSnapshot<TLRecord>;
initialData?: SerializedStore<TLRecord>;
persistenceKey?: string;
sessionId?: string;
@ -2607,6 +2609,7 @@ export function useIsEditing(shapeId: TLShapeId): boolean;
export function useLocalStore({ persistenceKey, sessionId, ...rest }: {
persistenceKey?: string;
sessionId?: string;
snapshot?: StoreSnapshot<TLRecord>;
} & TLStoreOptions): TLStoreWithStatus;
// @internal (undocumented)
@ -2630,7 +2633,9 @@ export function useSelectionEvents(handle: TLSelectionHandle): {
};
// @public (undocumented)
export function useTLStore(opts: TLStoreOptions): TLStore;
export function useTLStore(opts: TLStoreOptions & {
snapshot?: StoreSnapshot<TLRecord>;
}): TLStore;
// @public (undocumented)
export function useTransform(ref: React.RefObject<HTMLElement | SVGElement>, x?: number, y?: number, scale?: number, rotate?: number, additionalOffset?: VecLike): void;

Wyświetl plik

@ -1,4 +1,4 @@
import { SerializedStore, Store } from '@tldraw/store'
import { SerializedStore, Store, StoreSnapshot } from '@tldraw/store'
import { TLRecord, TLStore } from '@tldraw/tlschema'
import { Required, annotateError } from '@tldraw/utils'
import React, {
@ -47,6 +47,7 @@ export type TldrawEditorProps = TldrawEditorBaseProps &
}
| {
store?: undefined
snapshot?: StoreSnapshot<TLRecord>
initialData?: SerializedStore<TLRecord>
persistenceKey?: string
sessionId?: string
@ -187,7 +188,7 @@ export const TldrawEditor = memo(function TldrawEditor({
function TldrawEditorWithOwnStore(
props: Required<TldrawEditorProps & { store: undefined; user: TLUser }, 'shapeUtils' | 'tools'>
) {
const { defaultName, initialData, shapeUtils, persistenceKey, sessionId, user } = props
const { defaultName, snapshot, initialData, shapeUtils, persistenceKey, sessionId, user } = props
const syncedStore = useLocalStore({
shapeUtils,
@ -195,6 +196,7 @@ function TldrawEditorWithOwnStore(
persistenceKey,
sessionId,
defaultName,
snapshot,
})
return <TldrawEditorWithLoadingStore {...props} store={syncedStore} user={user} />

Wyświetl plik

@ -1,3 +1,5 @@
import { StoreSnapshot } from '@tldraw/store'
import { TLRecord } from '@tldraw/tlschema'
import { useEffect, useState } from 'react'
import { TLStoreOptions } from '../config/createTLStore'
import { TLStoreWithStatus } from '../utils/sync/StoreWithStatus'
@ -10,7 +12,11 @@ export function useLocalStore({
persistenceKey,
sessionId,
...rest
}: { persistenceKey?: string; sessionId?: string } & TLStoreOptions): TLStoreWithStatus {
}: {
persistenceKey?: string
sessionId?: string
snapshot?: StoreSnapshot<TLRecord>
} & TLStoreOptions): TLStoreWithStatus {
const [state, setState] = useState<{ id: string; storeWithStatus: TLStoreWithStatus } | null>(
null
)

Wyświetl plik

@ -1,9 +1,17 @@
import { StoreSnapshot } from '@tldraw/store'
import { TLRecord } from '@tldraw/tlschema'
import { useEffect, useRef, useState } from 'react'
import { TLStoreOptions, createTLStore } from '../config/createTLStore'
/** @public */
export function useTLStore(opts: TLStoreOptions) {
const [store, setStore] = useState(() => createTLStore(opts))
export function useTLStore(opts: TLStoreOptions & { snapshot?: StoreSnapshot<TLRecord> }) {
const [store, setStore] = useState(() => {
const store = createTLStore(opts)
if (opts.snapshot) {
store.loadSnapshot(opts.snapshot)
}
return store
})
// prev
const ref = useRef(opts)
useEffect(() => void (ref.current = opts))
@ -15,6 +23,9 @@ export function useTLStore(opts: TLStoreOptions) {
)
) {
const newStore = createTLStore(opts)
if (opts.snapshot) {
newStore.loadSnapshot(opts.snapshot)
}
setStore(newStore)
return newStore
}

Wyświetl plik

@ -40,6 +40,7 @@ import { SelectionHandle } from '@tldraw/editor';
import { SerializedSchema } from '@tldraw/editor';
import { ShapeUtil } from '@tldraw/editor';
import { StateNode } from '@tldraw/editor';
import { StoreSnapshot } from '@tldraw/editor';
import { SvgExportContext } from '@tldraw/editor';
import { TLAnyShapeUtilConstructor } from '@tldraw/editor';
import { TLArrowShape } from '@tldraw/editor';
@ -50,7 +51,7 @@ import { TLCancelEvent } from '@tldraw/editor';
import { TLClickEvent } from '@tldraw/editor';
import { TLClickEventInfo } from '@tldraw/editor';
import { TLDefaultSizeStyle } from '@tldraw/editor';
import { TldrawEditorProps } from '@tldraw/editor';
import { TldrawEditorBaseProps } from '@tldraw/editor';
import { TLDrawShape } from '@tldraw/editor';
import { TLDrawShapeSegment } from '@tldraw/editor';
import { TLEmbedShape } from '@tldraw/editor';
@ -80,6 +81,7 @@ import { TLParentId } from '@tldraw/editor';
import { TLPointerEvent } from '@tldraw/editor';
import { TLPointerEventInfo } from '@tldraw/editor';
import { TLPointerEventName } from '@tldraw/editor';
import { TLRecord } from '@tldraw/editor';
import { TLRotationSnapshot } from '@tldraw/editor';
import { TLSchema } from '@tldraw/editor';
import { TLScribble } from '@tldraw/editor';
@ -90,6 +92,7 @@ import { TLShapePartial } from '@tldraw/editor';
import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor';
import { TLShapeUtilFlag } from '@tldraw/editor';
import { TLStore } from '@tldraw/editor';
import { TLStoreWithStatus } from '@tldraw/editor';
import { TLTextShape } from '@tldraw/editor';
import { TLTickEvent } from '@tldraw/editor';
import { TLUnknownShape } from '@tldraw/editor';
@ -1092,7 +1095,15 @@ function Title({ className, children }: {
}): JSX.Element;
// @public (undocumented)
export function Tldraw(props: TldrawEditorProps & TldrawUiProps & Partial<TLExternalContentProps> & {
export function Tldraw(props: TldrawEditorBaseProps & ({
store: TLStore | TLStoreWithStatus;
} | {
store?: undefined;
persistenceKey?: string;
sessionId?: string;
defaultName?: string;
snapshot?: StoreSnapshot<TLRecord>;
}) & TldrawUiProps & Partial<TLExternalContentProps> & {
assetUrls?: RecursivePartial<TLEditorAssetUrls>;
}): JSX.Element;

Wyświetl plik

@ -4,8 +4,13 @@ import {
ErrorScreen,
LoadingScreen,
RecursivePartial,
StoreSnapshot,
TLOnMountHandler,
TLRecord,
TLStore,
TLStoreWithStatus,
TldrawEditor,
TldrawEditorBaseProps,
TldrawEditorProps,
assert,
useEditor,
@ -31,7 +36,22 @@ import { usePreloadAssets } from './utils/usePreloadAssets'
/** @public */
export function Tldraw(
props: TldrawEditorProps &
props: TldrawEditorBaseProps &
(
| {
store: TLStore | TLStoreWithStatus
}
| {
store?: undefined
persistenceKey?: string
sessionId?: string
defaultName?: string
/**
* A snapshot to load for the store's initial data / schema.
*/
snapshot?: StoreSnapshot<TLRecord>
}
) &
TldrawUiProps &
Partial<TLExternalContentProps> & {
/**