diff --git a/Readme.md b/Readme.md index 756d8f9af..54570981c 100644 --- a/Readme.md +++ b/Readme.md @@ -55,9 +55,9 @@ If you're using Slate for the first time, check out the [Getting Started](./docs - [**Guides**](docs/guides/installing-slate.md) - [Installing Slate](docs/guides/installing-slate.md) - [Adding Event Handlers](docs/guides/adding-event-handlers.md) - - [Adding Custom Formatting](docs/guides/adding-custom-formatting.md) - - [Adding Custom Block Types](docs/guides/adding-custom-block-types.md) - - Adding Plugins + - [Defining Custom Block Nodes](docs/guides/defining-custom-block-nodes.md) + - [Applying Custom Formatting](docs/guides/applying-custom-formatting.md) + - [Using Plugins](docs/guides/using-plugins.md) - [**Concepts**](docs/concepts.md) - Statelessness & Immutability diff --git a/docs/guides/Readme.md b/docs/guides/Readme.md index f69e8818a..45346a384 100644 --- a/docs/guides/Readme.md +++ b/docs/guides/Readme.md @@ -3,8 +3,8 @@ These guides introduce you to the different parts of Slate in a step-by-step way - [Installing Slate](./installing-slate.md) - [Adding Event Handlers](./adding-event-handlers.md) -- [Adding Custom Formatting](./adding-custom-formatting.md) -- [Adding Custom Block Types](./adding-custom-block-types.md) -- Adding Plugins +- [Defining Custom Block Nodes](./defining-custom-block-nodes.md) +- [Applying Custom Formatting](./applying-custom-formatting.md) +- [Using Plugins](./using-plugins.md) _If you have an idea for a guide, or notice something that isn't clear, submit a pull request!_ diff --git a/docs/guides/applying-custom-formatting.md b/docs/guides/applying-custom-formatting.md index 7c1df477f..988d4c46b 100644 --- a/docs/guides/applying-custom-formatting.md +++ b/docs/guides/applying-custom-formatting.md @@ -253,7 +253,7 @@ class App extends React.Component { const isBold = state.marks.some(mark => mark.type == 'bold') return state .transform() - [isBold ? 'mark' : 'unmark']('bold') + [isBold ? 'unmark' : 'mark']('bold') .apply() } case 192: { diff --git a/docs/guides/using-plugins.md b/docs/guides/using-plugins.md new file mode 100644 index 000000000..eb6f5ac60 --- /dev/null +++ b/docs/guides/using-plugins.md @@ -0,0 +1,338 @@ + +
+

Previous:
Applying Custom Formatting

+
+ +### Using Formatting + +Up to now, everything we've learned has been about how to write one-off logic for your specific Slate editor. But one of the most beautiful things about Slate is actually its plugin system, and how it lets you write less one-off code. + +In the previous guide, we actually wrote some pretty useful code for adding **bold** formatting to ranges of text when a key is pressed. But most of that code wasn't really specific to bold text. + +So let's look at how you'd break that code out into it's own reusable plugin that can toggle _any_ formatting mark on _any_ key press. + +Starting with our app from earlier: + +```js +const BOLD_MARK = { + fontWeight: 'bold' +} + +class App extends React.Component { + + constructor(props) { + super(props) + this.state = { + state: initialState + } + } + + render() { + return ( + this.renderMark(mark)} + renderNode={node => this.renderNode(node)} + onChange={state => this.onChange(state)} + onKeyDown={(e, state) => this.onKeyDown(e, state)} + /> + ) + } + + renderMark(mark) { + if (mark.type == 'bold') return BOLD_MARK + } + + renderNode(node) { + if (node.type == 'paragraph') return ParagraphNode + } + + onChange(state) { + this.setState({ state }) + } + + onKeyDown(event, state) { + if (!event.metaKey || event.which != 66) return + + const isBold = state.marks.some(mark => mark.type == 'bold') + return state + .transform() + [isBold ? 'unmark' : 'mark']('bold') + .apply() + } + +} +``` + +Let's write a new function, that takes a set of options: the mark `type` to toggle and the key `code` to press. + +```js +function MarkHotkey(options) { + // Grab our options from the ones passed in. + const { type, code } = options +} +``` + +Okay, that was easy. + +Now we want to have it return a dictionary of handlers, in this case specifically containing a `onKeyDown` handler with logic pulled from our app: + + +```js +function MarkHotkey(options) { + const { type, code } = options + + // Return our "plugin" object, containing the `onKeyDown` handler. + return { + onKeyDown(event, state) { + // Check that the key pressed matches our `code` option. + if (!event.metaKey || event.which != code) return + + // Determine whether our `type` option mark is currently active. + const isActive = state.marks.some(mark => mark.type == type) + + // Toggle the mark `type` based on whether it is active. + return state + .transform() + [isActive ? 'unmark' : 'mark'](type) + .apply() + } + } +} +``` + +Boom! Now we're getting somewhere. That code is reusable for any type of mark. + +So now, let's remove the old code from our app, and pass the editor our brand new `MarkHotkey` plugin instead, giving it the same options that will keep our bold functionality intact: + +```js +const BOLD_MARK = { + fontWeight: 'bold' +} + +// Initialize our bold-mark-adding plugin. +const boldPlugin = MarkHotkey({ + type: 'bold', + code: 66 +}) + +// Create an array of plugins. +const plugins = [ + boldPlugin +] + +class App extends React.Component { + + constructor(props) { + super(props) + this.state = { + state: initialState + } + } + + // Add the `plugins` property to the editor, and remove `onKeyDown`. + render() { + return ( + this.renderMark(mark)} + renderNode={node => this.renderNode(node)} + onChange={state => this.onChange(state)} + /> + ) + } + + renderMark(mark) { + if (mark.type == 'bold') return BOLD_MARK + } + + renderNode(node) { + if (node.type == 'paragraph') return ParagraphNode + } + + onChange(state) { + this.setState({ state }) + } + +} +``` + +Awesome. If you test out the editor now, you'll notice that everything still works just as it did before. But the beauty of the logic being encapsulated in a plugin is that we can add more mark types _extremely_ easily now! + +Let's add _italic_, `code`, ~~strikethrough~~ and underline marks: + +```js +// Add our new mark renderers... +const MARKS = { + bold: { + fontWeight: 'bold' + }, + code: { + fontFamily: 'monospace' + }, + italic: { + fontStyle: 'italic' + }, + strikethrough: { + textDecoration: 'strikethrough' + }, + underline: { + textDecoration: 'underline' + } +} + +// Initialize our plugins... +const plugins = [ + MarkHotkey({ code: 66, type: 'bold' }), + MarkHotkey({ code: 192, type: 'code' }), + MarkHotkey({ code: 73, type: 'italic' }), + MarkHotkey({ code: 68, type: 'strikethrough' }), + MarkHotkey({ code: 85, type: 'underline' }) +] + +class App extends React.Component { + + constructor(props) { + super(props) + this.state = { + state: initialState + } + } + + render() { + return ( + this.renderMark(mark)} + renderNode={node => this.renderNode(node)} + onChange={state => this.onChange(state)} + /> + ) + } + + // Update our render function to handle all the new marks... + renderMark(mark) { + return MARKS[mark.type] + } + + renderNode(node) { + if (node.type == 'paragraph') return ParagraphNode + } + + onChange(state) { + this.setState({ state }) + } + +} +``` + +And there you have it! We just added a ton of functionality to the editor with very little work. And we can keep all of our mark hotkey logic tested and isolated in a single place, making maintaining the code easier. + +Of course... now that it's reusable, we could actually make our `MarkHotkey` plugin a little easier to use. What if instead of a `code` argument it took the text of the `key` itself? That would make the calling code a lot clearer, since key codes are really obtuse. + +In fact, unless you have weirdly good keycode guessing, there's a good chance you had no idea what our current hotkeys bindings actually mapped to. + +Let's fix that. + +Using the `keycode` module in npm makes this dead simple. + +First install it: + +``` +npm install keycode +``` + +And then we can add it our plugin: + +```js +// Import the keycode module. +import keycode from `keycode` + +function MarkHotkey(options) { + // Change the options to take a `key`. + const { type, key } = options + + return { + onKeyDown(event, state) { + // Change the comparison to use the key name. + if (!event.metaKey || keycode(event.which) != key) return + + const isActive = state.marks.some(mark => mark.type == type) + return state + .transform() + [isActive ? 'unmark' : 'mark'](type) + .apply() + } + } +} +``` + +And finally, we can make our app code clearer: + +```js +const MARKS = { + bold: { + fontWeight: 'bold' + }, + code: { + fontFamily: 'monospace' + }, + italic: { + fontStyle: 'italic' + }, + strikethrough: { + textDecoration: 'strikethrough' + }, + underline: { + textDecoration: 'underline' + } +} + +// Use the much clearer key names instead of key codes! +const plugins = [ + MarkHotkey({ key: 'b', type: 'bold' }), + MarkHotkey({ key: '`', type: 'code' }), + MarkHotkey({ key: 'i', type: 'italic' }), + MarkHotkey({ key: 'd', type: 'strikethrough' }), + MarkHotkey({ key: 'u', type: 'underline' }) +] + +class App extends React.Component { + + constructor(props) { + super(props) + this.state = { + state: initialState + } + } + + render() { + return ( + this.renderMark(mark)} + renderNode={node => this.renderNode(node)} + onChange={state => this.onChange(state)} + /> + ) + } + + renderMark(mark) { + return MARKS[mark.type] + } + + renderNode(node) { + if (node.type == 'paragraph') return ParagraphNode + } + + onChange(state) { + this.setState({ state }) + } + +} +``` + +That's why plugins are awesome. They let you get really expressive while also making your codebase easier to manage. And since Slate is built with plugins as a primary consideration, using them is dead simple.