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
This commit is contained in:
parent
f21eaeb4d8
commit
0b3e83be52
11 changed files with 38344 additions and 10 deletions
|
@ -70,4 +70,9 @@ test.describe('Routes', () => {
|
||||||
await page.goto('http://localhost:5420/persistence')
|
await page.goto('http://localhost:5420/persistence')
|
||||||
await page.waitForSelector('.tl-canvas')
|
await page.waitForSelector('.tl-canvas')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('snapshots', async ({ page }) => {
|
||||||
|
await page.goto('http://localhost:5420/snapshots')
|
||||||
|
await page.waitForSelector('.tl-canvas')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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()`
|
38229
apps/examples/src/examples/SnapshotExample/snapshot.json
Normal file
38229
apps/examples/src/examples/SnapshotExample/snapshot.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -27,6 +27,7 @@ import MultipleExample from './examples/MultipleExample'
|
||||||
import PersistenceExample from './examples/PersistenceExample'
|
import PersistenceExample from './examples/PersistenceExample'
|
||||||
import ScrollExample from './examples/ScrollExample'
|
import ScrollExample from './examples/ScrollExample'
|
||||||
import ShapeMetaExample from './examples/ShapeMetaExample'
|
import ShapeMetaExample from './examples/ShapeMetaExample'
|
||||||
|
import SnapshotExample from './examples/SnapshotExample/SnapshotExample'
|
||||||
import StoreEventsExample from './examples/StoreEventsExample'
|
import StoreEventsExample from './examples/StoreEventsExample'
|
||||||
import UiEventsExample from './examples/UiEventsExample'
|
import UiEventsExample from './examples/UiEventsExample'
|
||||||
import UserPresenceExample from './examples/UserPresenceExample'
|
import UserPresenceExample from './examples/UserPresenceExample'
|
||||||
|
@ -135,6 +136,11 @@ export const allExamples: Example[] = [
|
||||||
path: '/persistence',
|
path: '/persistence',
|
||||||
element: <PersistenceExample />,
|
element: <PersistenceExample />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Snapshots',
|
||||||
|
path: '/snapshots',
|
||||||
|
element: <SnapshotExample />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Custom styles',
|
title: 'Custom styles',
|
||||||
path: '/custom-styles',
|
path: '/custom-styles',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"extends": "../../config/tsconfig.base.json",
|
"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"],
|
"exclude": ["node_modules", "dist", "**/*.css", ".tsbuild*", "./scripts/legacy-translations"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./.tsbuild"
|
"outDir": "./.tsbuild"
|
||||||
|
|
|
@ -31,6 +31,7 @@ import { SerializedStore } from '@tldraw/store';
|
||||||
import { ShapeProps } from '@tldraw/tlschema';
|
import { ShapeProps } from '@tldraw/tlschema';
|
||||||
import { Signal } from '@tldraw/state';
|
import { Signal } from '@tldraw/state';
|
||||||
import { StoreSchema } from '@tldraw/store';
|
import { StoreSchema } from '@tldraw/store';
|
||||||
|
import { StoreSnapshot } from '@tldraw/store';
|
||||||
import { StyleProp } from '@tldraw/tlschema';
|
import { StyleProp } from '@tldraw/tlschema';
|
||||||
import { TLArrowShape } from '@tldraw/tlschema';
|
import { TLArrowShape } from '@tldraw/tlschema';
|
||||||
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema';
|
import { TLArrowShapeArrowheadStyle } from '@tldraw/tlschema';
|
||||||
|
@ -2009,6 +2010,7 @@ export type TldrawEditorProps = TldrawEditorBaseProps & ({
|
||||||
store: TLStore | TLStoreWithStatus;
|
store: TLStore | TLStoreWithStatus;
|
||||||
} | {
|
} | {
|
||||||
store?: undefined;
|
store?: undefined;
|
||||||
|
snapshot?: StoreSnapshot<TLRecord>;
|
||||||
initialData?: SerializedStore<TLRecord>;
|
initialData?: SerializedStore<TLRecord>;
|
||||||
persistenceKey?: string;
|
persistenceKey?: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
|
@ -2607,6 +2609,7 @@ export function useIsEditing(shapeId: TLShapeId): boolean;
|
||||||
export function useLocalStore({ persistenceKey, sessionId, ...rest }: {
|
export function useLocalStore({ persistenceKey, sessionId, ...rest }: {
|
||||||
persistenceKey?: string;
|
persistenceKey?: string;
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
|
snapshot?: StoreSnapshot<TLRecord>;
|
||||||
} & TLStoreOptions): TLStoreWithStatus;
|
} & TLStoreOptions): TLStoreWithStatus;
|
||||||
|
|
||||||
// @internal (undocumented)
|
// @internal (undocumented)
|
||||||
|
@ -2630,7 +2633,9 @@ export function useSelectionEvents(handle: TLSelectionHandle): {
|
||||||
};
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function useTLStore(opts: TLStoreOptions): TLStore;
|
export function useTLStore(opts: TLStoreOptions & {
|
||||||
|
snapshot?: StoreSnapshot<TLRecord>;
|
||||||
|
}): TLStore;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
export function useTransform(ref: React.RefObject<HTMLElement | SVGElement>, x?: number, y?: number, scale?: number, rotate?: number, additionalOffset?: VecLike): void;
|
export function useTransform(ref: React.RefObject<HTMLElement | SVGElement>, x?: number, y?: number, scale?: number, rotate?: number, additionalOffset?: VecLike): void;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SerializedStore, Store } from '@tldraw/store'
|
import { SerializedStore, Store, StoreSnapshot } from '@tldraw/store'
|
||||||
import { TLRecord, TLStore } from '@tldraw/tlschema'
|
import { TLRecord, TLStore } from '@tldraw/tlschema'
|
||||||
import { Required, annotateError } from '@tldraw/utils'
|
import { Required, annotateError } from '@tldraw/utils'
|
||||||
import React, {
|
import React, {
|
||||||
|
@ -47,6 +47,7 @@ export type TldrawEditorProps = TldrawEditorBaseProps &
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
store?: undefined
|
store?: undefined
|
||||||
|
snapshot?: StoreSnapshot<TLRecord>
|
||||||
initialData?: SerializedStore<TLRecord>
|
initialData?: SerializedStore<TLRecord>
|
||||||
persistenceKey?: string
|
persistenceKey?: string
|
||||||
sessionId?: string
|
sessionId?: string
|
||||||
|
@ -187,7 +188,7 @@ export const TldrawEditor = memo(function TldrawEditor({
|
||||||
function TldrawEditorWithOwnStore(
|
function TldrawEditorWithOwnStore(
|
||||||
props: Required<TldrawEditorProps & { store: undefined; user: TLUser }, 'shapeUtils' | 'tools'>
|
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({
|
const syncedStore = useLocalStore({
|
||||||
shapeUtils,
|
shapeUtils,
|
||||||
|
@ -195,6 +196,7 @@ function TldrawEditorWithOwnStore(
|
||||||
persistenceKey,
|
persistenceKey,
|
||||||
sessionId,
|
sessionId,
|
||||||
defaultName,
|
defaultName,
|
||||||
|
snapshot,
|
||||||
})
|
})
|
||||||
|
|
||||||
return <TldrawEditorWithLoadingStore {...props} store={syncedStore} user={user} />
|
return <TldrawEditorWithLoadingStore {...props} store={syncedStore} user={user} />
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { StoreSnapshot } from '@tldraw/store'
|
||||||
|
import { TLRecord } from '@tldraw/tlschema'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { TLStoreOptions } from '../config/createTLStore'
|
import { TLStoreOptions } from '../config/createTLStore'
|
||||||
import { TLStoreWithStatus } from '../utils/sync/StoreWithStatus'
|
import { TLStoreWithStatus } from '../utils/sync/StoreWithStatus'
|
||||||
|
@ -10,7 +12,11 @@ export function useLocalStore({
|
||||||
persistenceKey,
|
persistenceKey,
|
||||||
sessionId,
|
sessionId,
|
||||||
...rest
|
...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>(
|
const [state, setState] = useState<{ id: string; storeWithStatus: TLStoreWithStatus } | null>(
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
|
import { StoreSnapshot } from '@tldraw/store'
|
||||||
|
import { TLRecord } from '@tldraw/tlschema'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { TLStoreOptions, createTLStore } from '../config/createTLStore'
|
import { TLStoreOptions, createTLStore } from '../config/createTLStore'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function useTLStore(opts: TLStoreOptions) {
|
export function useTLStore(opts: TLStoreOptions & { snapshot?: StoreSnapshot<TLRecord> }) {
|
||||||
const [store, setStore] = useState(() => createTLStore(opts))
|
const [store, setStore] = useState(() => {
|
||||||
|
const store = createTLStore(opts)
|
||||||
|
if (opts.snapshot) {
|
||||||
|
store.loadSnapshot(opts.snapshot)
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
})
|
||||||
// prev
|
// prev
|
||||||
const ref = useRef(opts)
|
const ref = useRef(opts)
|
||||||
useEffect(() => void (ref.current = opts))
|
useEffect(() => void (ref.current = opts))
|
||||||
|
@ -15,6 +23,9 @@ export function useTLStore(opts: TLStoreOptions) {
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
const newStore = createTLStore(opts)
|
const newStore = createTLStore(opts)
|
||||||
|
if (opts.snapshot) {
|
||||||
|
newStore.loadSnapshot(opts.snapshot)
|
||||||
|
}
|
||||||
setStore(newStore)
|
setStore(newStore)
|
||||||
return newStore
|
return newStore
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ import { SelectionHandle } from '@tldraw/editor';
|
||||||
import { SerializedSchema } from '@tldraw/editor';
|
import { SerializedSchema } from '@tldraw/editor';
|
||||||
import { ShapeUtil } from '@tldraw/editor';
|
import { ShapeUtil } from '@tldraw/editor';
|
||||||
import { StateNode } from '@tldraw/editor';
|
import { StateNode } from '@tldraw/editor';
|
||||||
|
import { StoreSnapshot } from '@tldraw/editor';
|
||||||
import { SvgExportContext } from '@tldraw/editor';
|
import { SvgExportContext } from '@tldraw/editor';
|
||||||
import { TLAnyShapeUtilConstructor } from '@tldraw/editor';
|
import { TLAnyShapeUtilConstructor } from '@tldraw/editor';
|
||||||
import { TLArrowShape } from '@tldraw/editor';
|
import { TLArrowShape } from '@tldraw/editor';
|
||||||
|
@ -50,7 +51,7 @@ import { TLCancelEvent } from '@tldraw/editor';
|
||||||
import { TLClickEvent } from '@tldraw/editor';
|
import { TLClickEvent } from '@tldraw/editor';
|
||||||
import { TLClickEventInfo } from '@tldraw/editor';
|
import { TLClickEventInfo } from '@tldraw/editor';
|
||||||
import { TLDefaultSizeStyle } from '@tldraw/editor';
|
import { TLDefaultSizeStyle } from '@tldraw/editor';
|
||||||
import { TldrawEditorProps } from '@tldraw/editor';
|
import { TldrawEditorBaseProps } from '@tldraw/editor';
|
||||||
import { TLDrawShape } from '@tldraw/editor';
|
import { TLDrawShape } from '@tldraw/editor';
|
||||||
import { TLDrawShapeSegment } from '@tldraw/editor';
|
import { TLDrawShapeSegment } from '@tldraw/editor';
|
||||||
import { TLEmbedShape } from '@tldraw/editor';
|
import { TLEmbedShape } from '@tldraw/editor';
|
||||||
|
@ -80,6 +81,7 @@ import { TLParentId } from '@tldraw/editor';
|
||||||
import { TLPointerEvent } from '@tldraw/editor';
|
import { TLPointerEvent } from '@tldraw/editor';
|
||||||
import { TLPointerEventInfo } from '@tldraw/editor';
|
import { TLPointerEventInfo } from '@tldraw/editor';
|
||||||
import { TLPointerEventName } from '@tldraw/editor';
|
import { TLPointerEventName } from '@tldraw/editor';
|
||||||
|
import { TLRecord } from '@tldraw/editor';
|
||||||
import { TLRotationSnapshot } from '@tldraw/editor';
|
import { TLRotationSnapshot } from '@tldraw/editor';
|
||||||
import { TLSchema } from '@tldraw/editor';
|
import { TLSchema } from '@tldraw/editor';
|
||||||
import { TLScribble } from '@tldraw/editor';
|
import { TLScribble } from '@tldraw/editor';
|
||||||
|
@ -90,6 +92,7 @@ import { TLShapePartial } from '@tldraw/editor';
|
||||||
import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor';
|
import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor';
|
||||||
import { TLShapeUtilFlag } from '@tldraw/editor';
|
import { TLShapeUtilFlag } from '@tldraw/editor';
|
||||||
import { TLStore } from '@tldraw/editor';
|
import { TLStore } from '@tldraw/editor';
|
||||||
|
import { TLStoreWithStatus } from '@tldraw/editor';
|
||||||
import { TLTextShape } from '@tldraw/editor';
|
import { TLTextShape } from '@tldraw/editor';
|
||||||
import { TLTickEvent } from '@tldraw/editor';
|
import { TLTickEvent } from '@tldraw/editor';
|
||||||
import { TLUnknownShape } from '@tldraw/editor';
|
import { TLUnknownShape } from '@tldraw/editor';
|
||||||
|
@ -1092,7 +1095,15 @@ function Title({ className, children }: {
|
||||||
}): JSX.Element;
|
}): JSX.Element;
|
||||||
|
|
||||||
// @public (undocumented)
|
// @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>;
|
assetUrls?: RecursivePartial<TLEditorAssetUrls>;
|
||||||
}): JSX.Element;
|
}): JSX.Element;
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,13 @@ import {
|
||||||
ErrorScreen,
|
ErrorScreen,
|
||||||
LoadingScreen,
|
LoadingScreen,
|
||||||
RecursivePartial,
|
RecursivePartial,
|
||||||
|
StoreSnapshot,
|
||||||
TLOnMountHandler,
|
TLOnMountHandler,
|
||||||
|
TLRecord,
|
||||||
|
TLStore,
|
||||||
|
TLStoreWithStatus,
|
||||||
TldrawEditor,
|
TldrawEditor,
|
||||||
|
TldrawEditorBaseProps,
|
||||||
TldrawEditorProps,
|
TldrawEditorProps,
|
||||||
assert,
|
assert,
|
||||||
useEditor,
|
useEditor,
|
||||||
|
@ -31,7 +36,22 @@ import { usePreloadAssets } from './utils/usePreloadAssets'
|
||||||
|
|
||||||
/** @public */
|
/** @public */
|
||||||
export function Tldraw(
|
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 &
|
TldrawUiProps &
|
||||||
Partial<TLExternalContentProps> & {
|
Partial<TLExternalContentProps> & {
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue