1
0
mirror of https://github.com/ianstormtaylor/slate.git synced 2025-08-18 05:01:17 +02:00

Change <Slate> to a controlled component (#3216)

* change <Slate> to be a controlled component

* add comment about unstable React API
This commit is contained in:
Ian Storm Taylor
2019-12-05 11:36:44 -05:00
committed by GitHub
parent 4c03b497d9
commit f3fc2c2a54
29 changed files with 305 additions and 110 deletions

View File

@@ -18,7 +18,6 @@
"@types/debug": "^4.1.5",
"@types/is-hotkey": "^0.1.1",
"@types/lodash": "^4.14.149",
"@types/react": "^16.9.13",
"debounce": "^1.2.0",
"direction": "^1.0.3",
"is-hotkey": "^0.1.6",

View File

@@ -0,0 +1,41 @@
import React, { useMemo } from 'react'
import { Editor, Node, Range } from 'slate'
import { ReactEditor } from '../plugin/react-editor'
import { FocusedContext } from '../hooks/use-focused'
import { EditorContext } from '../hooks/use-editor'
import { SlateContext } from '../hooks/use-slate'
import { EDITOR_TO_ON_CHANGE } from '../utils/weak-maps'
/**
* A wrapper around the provider to handle `onChange` events, because the editor
* is a mutable singleton so it won't ever register as "changed" otherwise.
*/
export const Slate = (props: {
editor: Editor
value: Node[]
selection: Range | null
children: React.ReactNode
onChange: (children: Node[], selection: Range | null) => void
[key: string]: any
}) => {
const { editor, children, onChange, value, selection, ...rest } = props
const context: [Editor] = useMemo(() => {
editor.children = value
editor.selection = selection
return [editor]
}, [value, selection, ...Object.values(rest)])
EDITOR_TO_ON_CHANGE.set(editor, onChange)
return (
<SlateContext.Provider value={context}>
<EditorContext.Provider value={editor}>
<FocusedContext.Provider value={ReactEditor.isFocused(editor)}>
{children}
</FocusedContext.Provider>
</EditorContext.Provider>
</SlateContext.Provider>
)
}

View File

@@ -1,18 +1,5 @@
import React, { useState, useMemo } from 'react'
import { Editor, Node, Operation } from 'slate'
import { Editor } from 'slate'
import { createContext, useContext } from 'react'
import { ReactEditor } from '../react-editor'
import { FocusedContext } from './use-focused'
import { EditorContext } from './use-editor'
/**
* Associate the context change listener with the editor.
*/
export const EDITOR_TO_CONTEXT_LISTENER = new WeakMap<
Editor,
(children: Node[], operations: Operation[]) => void
>()
/**
* A React context for sharing the `Editor` class, in a way that re-renders the
@@ -21,42 +8,6 @@ export const EDITOR_TO_CONTEXT_LISTENER = new WeakMap<
export const SlateContext = createContext<[Editor] | null>(null)
/**
* A wrapper around the provider to handle `onChange` events, because the editor
* is a mutable singleton so it won't ever register as "changed" otherwise.
*/
export const Slate = (props: {
editor: Editor
children: React.ReactNode
defaultValue?: Node[]
onChange?: (children: Node[], operations: Operation[]) => void
}) => {
const { editor, children, defaultValue = [], onChange = () => {} } = props
const [context, setContext] = useState([editor])
const value: [Editor] = useMemo(() => [editor], [context, editor])
const listener = useMemo(() => {
editor.children = defaultValue
return (children: Node[], operations: Operation[]) => {
onChange(children, operations)
setContext([editor])
}
}, [editor])
EDITOR_TO_CONTEXT_LISTENER.set(editor, listener)
return (
<SlateContext.Provider value={value}>
<EditorContext.Provider value={editor}>
<FocusedContext.Provider value={ReactEditor.isFocused(editor)}>
{children}
</FocusedContext.Provider>
</EditorContext.Provider>
</SlateContext.Provider>
)
}
/**
* Get the current `Editor` class that the component lives under.
*/

View File

@@ -1,11 +1,21 @@
export * from './components/editable'
// Components
export {
RenderElementProps,
RenderLeafProps,
Editable,
} from './components/editable'
export { DefaultElement } from './components/element'
export { DefaultLeaf } from './components/leaf'
export * from './hooks/use-editor'
export * from './hooks/use-focused'
export * from './hooks/use-read-only'
export * from './hooks/use-selected'
export * from './hooks/use-slate'
export * from './react-command'
export * from './react-editor'
export * from './with-react'
export { Slate } from './components/slate'
// Hooks
export { useEditor } from './hooks/use-editor'
export { useFocused } from './hooks/use-focused'
export { useReadOnly } from './hooks/use-read-only'
export { useSelected } from './hooks/use-selected'
export { useSlate } from './hooks/use-slate'
// Plugin
export { InsertDataCommand, ReactCommand } from './plugin/react-command'
export { ReactEditor } from './plugin/react-editor'
export { withReact } from './plugin/with-react'

View File

@@ -1,6 +1,6 @@
import { Editor, Element, Node, Path, Point, Range } from 'slate'
import { Key } from './utils/key'
import { Key } from '../utils/key'
import {
EDITOR_TO_ELEMENT,
ELEMENT_TO_NODE,
@@ -10,8 +10,7 @@ import {
NODE_TO_INDEX,
NODE_TO_KEY,
NODE_TO_PARENT,
PLACEHOLDER_SYMBOL,
} from './utils/weak-maps'
} from '../utils/weak-maps'
import {
DOMElement,
DOMNode,
@@ -21,7 +20,7 @@ import {
DOMStaticRange,
isDOMElement,
normalizeDOMPoint,
} from './utils/dom'
} from '../utils/dom'
export interface ReactEditor extends Editor {}

View File

@@ -1,9 +1,10 @@
import { unstable_batchedUpdates } from 'react-dom'
import { Editor, Node, Path, Operation, Command } from 'slate'
import { ReactEditor, ReactCommand } from '.'
import { Key } from './utils/key'
import { NODE_TO_KEY } from './utils/weak-maps'
import { EDITOR_TO_CONTEXT_LISTENER } from './hooks/use-slate'
import { ReactEditor } from './react-editor'
import { ReactCommand } from './react-command'
import { Key } from '../utils/key'
import { EDITOR_TO_ON_CHANGE, NODE_TO_KEY } from '../utils/weak-maps'
/**
* `withReact` adds React and DOM specific behaviors to the editor.
@@ -87,14 +88,21 @@ export const withReact = (editor: Editor): Editor => {
}
}
editor.onChange = (children: Node[], operations: Operation[]) => {
const contextOnChange = EDITOR_TO_CONTEXT_LISTENER.get(editor)
editor.onChange = () => {
// COMPAT: React doesn't batch `setState` hook calls, which means that the
// children and selection can get out of sync for one render pass. So we
// have to use this unstable API to ensure it batches them. (2019/12/03)
// https://github.com/facebook/react/issues/14259#issuecomment-439702367
unstable_batchedUpdates(() => {
const contextOnChange = EDITOR_TO_ON_CHANGE.get(editor)
if (contextOnChange) {
contextOnChange(children, operations)
}
if (contextOnChange) {
const { children, selection } = editor
contextOnChange(children, selection)
}
onChange(children, operations)
onChange()
})
}
return editor

View File

@@ -1,4 +1,4 @@
import { Node, Ancestor, Editor, Text } from 'slate'
import { Node, Ancestor, Editor, Range } from 'slate'
import { Key } from './key'
@@ -31,4 +31,17 @@ export const IS_FOCUSED: WeakMap<Editor, boolean> = new WeakMap()
export const IS_DRAGGING: WeakMap<Editor, boolean> = new WeakMap()
export const IS_CLICKING: WeakMap<Editor, boolean> = new WeakMap()
/**
* Weak map for associating the context `onChange` prop with the plugin.
*/
export const EDITOR_TO_ON_CHANGE = new WeakMap<
Editor,
(children: Node[], selection: Range | null) => void
>()
/**
* Symbols.
*/
export const PLACEHOLDER_SYMBOL = (Symbol('placeholder') as unknown) as string