diff --git a/docs/guides/saving-to-a-database.md b/docs/guides/saving-to-a-database.md index 96ee2e2d2..6ba5c97ef 100644 --- a/docs/guides/saving-to-a-database.md +++ b/docs/guides/saving-to-a-database.md @@ -81,7 +81,6 @@ Now whenever you edit the page, if you look in Local Storage, you should see the But... if you refresh the page, everything is still reset. That's because we need to make sure the initial state is pulled from that same Local Storage location, like so: - ```js class App extends React.Component { @@ -114,3 +113,40 @@ class App extends React.Component { Now you should be able to save changes across refreshes! +However, if you inspect the change handler, you'll notice that it's actually saving the Local Storage value on _every_ change to the editor, even when only the selection changes! This is because `onChange` is called for _every_ change. For Local Storage this doesn't really matter, but if you're saving things to a database via HTTP request this would result in a lot of unnecessary requests. + +Instead of using `onChange`, Slate's editor also accepts an `onDocumentChange` convenience handler that you can use to isolate saving logic to only happen when the document itself has changed, like so: + +```js +class App extends React.Component { + + constructor(props) { + super(props) + this.state = { + // Update the initial value to be pulled from Local Storage. + state: Plain.deserialize(localStorage.getItem('content')) + } + } + + render() { + return ( + this.onChange(state)} + /> + ) + } + + onChange(state) { + this.setState({ state }) + } + + onDocumentChange(document, state) { + const string = Plain.serialize(state) + localStorage.setItem('content', string) + } + +} +``` + +Now you're content will be saved only when the content itself changes! diff --git a/docs/reference/components/editor.md b/docs/reference/components/editor.md index 616ad7f91..d6c14661c 100644 --- a/docs/reference/components/editor.md +++ b/docs/reference/components/editor.md @@ -10,6 +10,8 @@ The top-level React component that renders the Slate editor itself. - [Properties](#properties) - [`className`](#classname) - [`onChange`](#onchange) + - [`onDocumentChange`](#ondocumentchange) + - [`onSelectionChange`](#onselectionchange) - [`plugins`](#plugins) - [`readOnly`](#readonly) - [`state`](#state) @@ -49,10 +51,20 @@ The top-level React component that renders the Slate editor itself. An optional class name to apply to the content editable element. ### `onChange` -`Function` +`Function onChange(state: State)` A change handler that will be called with the newly-changed editor `state`. You should usually pass the newly changed `state` back into the editor through its `state` property. This hook allows you to add persistence logic to your editor. +### `onDocumentChange` +`Function onDocumentChange(document: Document, state: State)` + +A convenience handler property that will only be called for changes in state where the document has changed. It is called with the changed `document` and `state`. + +### `onSelectionChange` +`Function onSelectionChange(selection: Selection, state: State)` + +A convenience handler property that will only be called for changes in state where the selection has changed. It is called with the changed `selection` and `state`. + ### `plugins` `Array` diff --git a/docs/reference/plugins/plugins.md b/docs/reference/plugins/plugins.md index 0856c5724..704ee821e 100644 --- a/docs/reference/plugins/plugins.md +++ b/docs/reference/plugins/plugins.md @@ -48,21 +48,21 @@ All of the event handler properties are passed the same React `event` object you Each event handler can choose to return a new `state` object, in which case the editor's state will be updated. If nothing is returned, the editor will simply continue resolving the plugin stack. ### `onBeforeInput` -`onBeforeInput(event: Event, state: State, editor: Editor) => State || Void` +`Function onBeforeInput(event: Event, state: State, editor: Editor) => State || Void` This handler is called right before a string of text is inserted into the `contenteditable` element. The `event.data` property will be the string of text that is being inserted. Make sure to `event.preventDefault()` if you do not want the default insertion behavior to occur! If no other plugin handles this event, it will be handled by the [Core plugin](./core.md). ### `onKeyDown` -`onKeyDown(event: Event, state: State, editor: Editor) => State || Void` +`Function onKeyDown(event: Event, state: State, editor: Editor) => State || Void` This handler is called when any key is pressed in the `contenteditable` element, before any action is taken. Use the `event.which` property to determine which key was pressed. Make sure to `event.preventDefault()` if you do not want the default insertion behavior to occur! If no other plugin handles this event, it will be handled by the [Core plugin](./core.md). ### `onPaste` -`onPaste(event: Event, paste: Object, state: State, editor: Editor) => State || Void` +`Function onPaste(event: Event, paste: Object, state: State, editor: Editor) => State || Void` This handler is called when the user pastes content into the `contenteditable` element. The event is already prevented by default, so you must define a state change to have any affect occur. @@ -102,14 +102,14 @@ If no other plugin handles this event, it will be handled by the [Core plugin](. To customize the renderer output of the editor, plugins can define a set of "renderer" properties. ### `renderDecorations` -`renderDecorations(text: Text) => Characters || Void` +`Function renderDecorations(text: Text) => Characters || Void` The `renderDecorations` handler allows you to add dynamic, content-aware [`Marks`](../models/mark.md) to ranges of text, without having them show up in the serialized state of the editor. This is useful for things like code highlighting, where the marks will change as the user types. `renderDecorations` is called for every `text` node in the document, and should return a set of updated [`Characters`](../models/character.md) for the text node in question. Every plugin's decoration logic is called, and the resulting characters are unioned, such that multiple plugins can apply decorations to the same pieces of text. ### `renderMark` -`renderMark(mark: Mark) => Object || Void` +`Function renderMark(mark: Mark) => Object || Void` The `renderMark` handler allows you to define the styles that each mark should be rendered with. It takes a [`Mark`](../models/mark.md) object, and should return a dictionary of styles that will be applied via React's `style=` property. For example: @@ -121,7 +121,7 @@ The `renderMark` handler allows you to define the styles that each mark should b ``` ### `renderNode` -`renderNode(node: Block || Inline) => Component || Void` +`Function renderNode(node: Block || Inline) => Component || Void` The `renderNode` handler allows you to define the component that will be used to render a node—both blocks and inlines. It takes a [`Node`](../models/node.md) object, and should return a React component. @@ -167,7 +167,7 @@ The `node` itself is passed in, so you can access any custom data associated wit ``` ### `onChange` -`onChange(state: State) => State || Void` +`Function onChange(state: State) => State || Void` The `onChange` handler isn't a native browser event handler. Instead, it is invoked whenever the editor state changes. Returning a new state will update the editor's state, continuing down the plugin stack. diff --git a/lib/components/editor.js b/lib/components/editor.js index 9232039fb..1fea70f0e 100644 --- a/lib/components/editor.js +++ b/lib/components/editor.js @@ -4,6 +4,12 @@ import CorePlugin from '../plugins/core' import React from 'react' import State from '../models/state' +/** + * Noop. + */ + +function noop() {} + /** * Editor. */ @@ -18,8 +24,10 @@ class Editor extends React.Component { className: React.PropTypes.string, onBeforeInput: React.PropTypes.func, onChange: React.PropTypes.func.isRequired, + onDocumentChange: React.PropTypes.func, onKeyDown: React.PropTypes.func, onPaste: React.PropTypes.func, + onSelectionChange: React.PropTypes.func, placeholder: React.PropTypes.any, placeholderClassName: React.PropTypes.string, placeholderStyle: React.PropTypes.object, @@ -33,6 +41,8 @@ class Editor extends React.Component { }; static defaultProps = { + onDocumentChange: noop, + onSelectionChange: noop, plugins: [], readOnly: false }; @@ -45,6 +55,7 @@ class Editor extends React.Component { constructor(props) { super(props) + this.tmp = {} this.state = {} this.state.plugins = this.resolvePlugins(props) this.state.state = this.resolveState(props.state) @@ -119,6 +130,16 @@ class Editor extends React.Component { } this.props.onChange(state) + + if (state.document != this.tmp.document) { + this.props.onDocumentChange(state.document, state) + this.tmp.document = state.document + } + + if (state.selection != this.tmp.selection) { + this.props.onSelectionChange(state.selection, state) + this.tmp.selection = state.selection + } } /**