Add example for external UI (#2846)

This PR adds an example for how to add external UI components.

It's something that I've been asked for help with in the past, and it's
something I wish more consumers would do when using tldraw inline - it
would help the editor feel less cramped. Therefore, I'd like to have it
is as an example we can point people to.

![2024-02-19 at 11 53 18 - Maroon
Boar](https://github.com/tldraw/tldraw/assets/15892272/f043bb77-3181-4a7d-b736-2b6a5012e208)

> Some other context:
> We've talked about giving this sorta thing more first-class support in
the future, eg: with a `useEditorInstance` hook or something.

Closes TLD-2128

### Change Type

- [x] `documentation` — Changes to the documentation only[^2]

[^1]: publishes a `patch` release, for devDependencies use `internal`
[^2]: will not publish a new version

### Test Plan

1. Try out the External UI example.
2. Make sure you can change tool by pressing the toolbar buttons.
3. Make sure you can change tool by pressing keys (eg: d, e)

- [ ] Unit Tests
- [ ] End to end tests

### Release Notes

- Docs: Added external UI example.

---------

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
Lu Wilson 2024-02-23 16:07:08 +00:00 committed by GitHub
parent d731951fcf
commit 7a09581081
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 202 additions and 3 deletions

View file

@ -0,0 +1,74 @@
import { Editor, Tldraw, useValue } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { createContext, useContext, useState } from 'react'
import './external-ui.css'
// There's a guide at the bottom of this file!
// [1]
const editorContext = createContext({} as { editor: Editor })
export default function ExternalUiExample2() {
const [editor, setEditor] = useState<Editor | null>(null)
return (
<div style={{ margin: 32, width: 600 }}>
<div style={{ height: 400 }}>
<Tldraw
// [2]
onMount={(editor) => setEditor(editor)}
components={{ Toolbar: null }}
/>
</div>
{/* [3] */}
{editor && (
<editorContext.Provider value={{ editor }}>
<ExternalToolbar />
</editorContext.Provider>
)}
</div>
)
}
// [4]
const ExternalToolbar = () => {
const { editor } = useContext(editorContext)
const currentToolId = useValue('current tool id', () => editor?.getCurrentToolId(), [editor])
return (
<div>
<div className="external-toolbar">
<button
className="external-button"
data-isactive={currentToolId === 'select'}
onClick={() => editor.setCurrentTool('select')}
>
Select
</button>
<button
className="external-button"
data-isactive={currentToolId === 'draw'}
onClick={() => editor.setCurrentTool('draw')}
>
Pencil
</button>
</div>
</div>
)
}
/*
[1]
Use React context to store the editor at a higher place in the React component tree.
[2]
Use the `onMount` prop to get the editor instance and store it in state.
[3]
When we have an editor in state, render the context provider and its descendants.
[4]
You can access the editor from any of the provider's descendants.
*/

View file

@ -0,0 +1,15 @@
---
title: External UI (Context)
component: ./ExternalUiContextExample.tsx
category: ui
---
This example shows how to control the tldraw editor from an external UI, outside of the `Tldraw` component. This example shows how to pass a reference
to the editor around using React Context.
---
This example shows how to control the tldraw editor from an external UI, outside
of the `Tldraw` component. There are a few ways of doing this—for example, by putting the editor on the window object, passing it around via props, or using React context.
In this example, we use React context to distribute a reference to the editor to child components.

View file

@ -0,0 +1,20 @@
.external-toolbar {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
gap: 8px;
}
.external-button {
pointer-events: all;
padding: 4px 12px;
background: white;
border: 1px solid black;
border-radius: 16px;
}
.external-button[data-isactive='true'] {
background-color: black;
color: white;
}

View file

@ -0,0 +1,56 @@
import { Editor, Tldraw, useValue } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { useState } from 'react'
import './external-ui.css'
// There's a guide at the bottom of this file!
export default function ExternalUiExample() {
// [1]
const [editor, setEditor] = useState<Editor | null>(null)
const currentToolId = useValue('current tool id', () => editor?.getCurrentToolId(), [editor])
return (
<div style={{ margin: 32, width: 600 }}>
<div style={{ height: 400 }}>
<Tldraw
// [2]
onMount={(editor) => setEditor(editor)}
components={{ Toolbar: null }}
/>
</div>
{/* [3] */}
<div>
<div className="external-toolbar">
<button
className="external-button"
data-isactive={currentToolId === 'select'}
onClick={() => editor?.setCurrentTool('select')}
>
Select
</button>
<button
className="external-button"
data-isactive={currentToolId === 'draw'}
onClick={() => editor?.setCurrentTool('draw')}
>
Pencil
</button>
</div>
</div>
</div>
)
}
/*
[1]
Use React state to store the editor instance.
[2]
Use the `onMount` prop to get the editor instance and store it in state.
[3]
Use data from the editor instance or use the editor's methods to control the editor.
Note that these callbacks also need to work if the editor isn't mounted yet.
*/

View file

@ -0,0 +1,14 @@
---
title: External UI
component: ./ExternalUiExample.tsx
category: ui
---
This example shows how to control the tldraw editor from an external UI, outside of the `Tldraw` component.
---
This example shows how to control the tldraw editor from an external UI, outside
of the `Tldraw` component. There are a few ways of doing this—for example, by putting the editor on the window object, passing it around via props, or using React context.
In this example, we'll just put the editor instance in state and use it in the same component. See the External UI Example 2 for an alternative (and more realistic) solution using React context.

View file

@ -0,0 +1,20 @@
.external-toolbar {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
gap: 8px;
}
.external-button {
pointer-events: all;
padding: 4px 12px;
background: white;
border: 1px solid black;
border-radius: 16px;
}
.external-button[data-isactive='true'] {
background-color: black;
color: white;
}

View file

@ -102,6 +102,6 @@ and viewing it.
default, the first page is shown. default, the first page is shown.
[3] You can customize the appearance of the image by passing other props to the [3] You can customize the appearance of the image by passing other props to the
`TldrawImage` component. For example, you can toggle the background, set the `TldrawImage` component. For example, you can toggle the background, set
dark mode, and specify the viewport bounds. the dark mode, and specify the viewport bounds.
*/ */