Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change <Slate> to a controlled component #3216

Merged
merged 3 commits into from
Dec 5, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/concepts/07-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface Editor {
isInline: (element: Element) => boolean
isVoid: (element: Element) => boolean
normalizeNode: (entry: NodeEntry) => void
onChange: (children: Node[], operations: Operation[]) => void
onChange: () => void
children: Node[]
operations: Operation[]
selection: Range | null
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@types/mocha": "^5.2.7",
"@types/node": "^12.12.14",
"@types/react": "^16.9.13",
"@types/react-dom": "^16.9.4",
"@typescript-eslint/eslint-plugin": "^2.9.0",
"@typescript-eslint/parser": "^2.9.0",
"babel-eslint": "^10.0.3",
Expand Down
1 change: 0 additions & 1 deletion packages/slate-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
41 changes: 41 additions & 0 deletions packages/slate-react/src/components/slate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useMemo } from 'react'
import { Editor, Node, Operation, Range } from 'slate'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moving this to a different module was a good decision i think, it took a little bit to find it just diving through github.


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: JSX.Element | JSX.Element[]
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)])

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if there will be some gotchas around memoizing around the extra props. for the most part this is probably what a caller is going to need but this seems like a subtle trap for users who are not paying attention

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, although by including them it's less restrictive (not more) so I think it's alright? Otherwise implementing your own custom editor properties becomes impossible I think?

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>
)
}
51 changes: 1 addition & 50 deletions packages/slate-react/src/hooks/use-slate.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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: JSX.Element | JSX.Element[]
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.
*/
Expand Down
29 changes: 20 additions & 9 deletions packages/slate-react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
export * from './components/editable'
// Components
export {
RenderDecorationProps,
RenderElementProps,
RenderMarkProps,
Editable,
} from './components/editable'
export { DefaultElement } from './components/element'
export { DefaultMark, DefaultDecoration } 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'
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -21,7 +20,7 @@ import {
DOMStaticRange,
isDOMElement,
normalizeDOMPoint,
} from './utils/dom'
} from '../utils/dom'

export interface ReactEditor extends Editor {}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -90,14 +91,21 @@ export const withReact = (editor: Editor): Editor => {
}
}

editor.onChange = (children: Node[], operations: Operation[]) => {
const contextOnChange = EDITOR_TO_CONTEXT_LISTENER.get(editor)

if (contextOnChange) {
contextOnChange(children, operations)
}
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) {
const { children, selection } = editor
contextOnChange(children, selection)
}

onChange(children, operations)
onChange()
})
}

return editor
Expand Down
11 changes: 10 additions & 1 deletion packages/slate-react/src/utils/weak-maps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Node, Ancestor, Editor } from 'slate'
import { Node, Ancestor, Editor, Range } from 'slate'

import { Key } from './key'

Expand Down Expand Up @@ -30,6 +30,15 @@ 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.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/slate/src/create-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export const createEditor = (): Editor => {

Promise.resolve().then(() => {
FLUSHING.set(editor, false)
editor.onChange(editor.children, editor.operations)
editor.onChange()
editor.operations = []
})
}
Expand Down
2 changes: 1 addition & 1 deletion packages/slate/src/interfaces/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface Editor {
isInline: (element: Element) => boolean
isVoid: (element: Element) => boolean
normalizeNode: (entry: NodeEntry) => void
onChange: (children: Node[], operations: Operation[]) => void
onChange: () => void
operations: Operation[]
selection: Range | null
[key: string]: any
Expand Down
15 changes: 13 additions & 2 deletions site/examples/check-lists.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import React, { useMemo, useCallback } from 'react'
import React, { useState, useMemo, useCallback } from 'react'
import { Slate, Editable, withReact, useEditor, useReadOnly } from 'slate-react'
import { Editor, Range, Point, createEditor } from 'slate'
import { css } from 'emotion'
import { withHistory } from 'slate-history'

const CheckListsExample = () => {
const [value, setValue] = useState(initialValue)
const [selection, setSelection] = useState(null)
const renderElement = useCallback(props => <Element {...props} />, [])
const editor = useMemo(
() => withChecklists(withHistory(withReact(createEditor()))),
[]
)

return (
<Slate editor={editor} defaultValue={initialValue}>
<Slate
editor={editor}
value={value}
selection={selection}
onChange={(value, selection) => {
setValue(value)
setSelection(selection)
}}
>
<Editable
renderElement={renderElement}
placeholder="Get to work…"
Expand Down
14 changes: 12 additions & 2 deletions site/examples/embeds.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react'
import React, { useState, useMemo } from 'react'
import { Editor, createEditor } from 'slate'
import {
Slate,
Expand All @@ -10,9 +10,19 @@ import {
} from 'slate-react'

const EmbedsExample = () => {
const [value, setValue] = useState(initialValue)
const [selection, setSelection] = useState(null)
const editor = useMemo(() => withEmbeds(withReact(createEditor())), [])
return (
<Slate editor={editor} defaultValue={initialValue}>
<Slate
editor={editor}
value={value}
selection={selection}
onChange={(value, selection) => {
setValue(value)
setSelection(selection)
}}
>
<Editable
renderElement={props => <Element {...props} />}
placeholder="Enter some text..."
Expand Down
14 changes: 12 additions & 2 deletions site/examples/forced-layout.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react'
import React, { useState, useCallback, useMemo } from 'react'
import { Slate, Editable, withReact } from 'slate-react'
import { Editor, createEditor } from 'slate'
import { withHistory } from 'slate-history'
Expand Down Expand Up @@ -38,13 +38,23 @@ const schema = [
]

const ForcedLayoutExample = () => {
const [value, setValue] = useState(initialValue)
const [selection, setSelection] = useState(null)
const renderElement = useCallback(props => <Element {...props} />, [])
const editor = useMemo(
() => withSchema(withHistory(withReact(createEditor())), schema),
[]
)
return (
<Slate editor={editor} defaultValue={initialValue}>
<Slate
editor={editor}
value={value}
selection={selection}
onChange={(value, selection) => {
setValue(value)
setSelection(selection)
}}
>
<Editable
renderElement={renderElement}
placeholder="Enter a title…"
Expand Down
15 changes: 13 additions & 2 deletions site/examples/hovering-toolbar.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo, useRef, useEffect } from 'react'
import React, { useState, useMemo, useRef, useEffect } from 'react'
import { Slate, Editable, ReactEditor, withReact, useSlate } from 'slate-react'
import { Editor, createEditor } from 'slate'
import { css } from 'emotion'
Expand All @@ -8,12 +8,23 @@ import { Button, Icon, Menu, Portal } from '../components'
import { Range } from 'slate'

const HoveringMenuExample = () => {
const [value, setValue] = useState(initialValue)
const [selection, setSelection] = useState(null)
const editor = useMemo(
() => withMarks(withHistory(withReact(createEditor()))),
[]
)

return (
<Slate editor={editor} defaultValue={initialValue}>
<Slate
editor={editor}
value={value}
selection={selection}
onChange={(value, selection) => {
setValue(value)
setSelection(selection)
}}
>
<HoveringToolbar />
<Editable
renderMark={props => <Mark {...props} />}
Expand Down
Loading