From 48eb6700b7fbd825383f39b788072a4ad3486601 Mon Sep 17 00:00:00 2001 From: Sunny Hirai Date: Thu, 16 May 2019 16:26:13 -0700 Subject: [PATCH] Add error boundary to save Slate from a crash on corrupt DOM (#2792) * Working version of restore dom * Fix linting errors * Add button to corrupt DOM * Added error boundary that fixes DOM on render error * Fix linting errors * Fix debug output for componentDidCatch * Improve example by adding a separate restoreDOM button * Remove key change from error boundary which is not necessary * Fix linting error --- examples/restore-dom/index.js | 90 +++++++++++++++++-- examples/restore-dom/value.json | 33 ++++++- .../slate-react/src/components/content.js | 16 ++++ 3 files changed, 130 insertions(+), 9 deletions(-) diff --git a/examples/restore-dom/index.js b/examples/restore-dom/index.js index 645939904..c8a572066 100644 --- a/examples/restore-dom/index.js +++ b/examples/restore-dom/index.js @@ -3,7 +3,7 @@ import { Value } from 'slate' import React from 'react' import initialValue from './value.json' -import { Button, Icon, Toolbar } from '../components' +import { Button, EditorValue, Icon, Instruction, Toolbar } from '../components' /** * The Restore DOM example. @@ -31,7 +31,7 @@ class RestoreDOMExample extends React.Component { state = { value: Value.fromJSON(initialValue), - bgcolor: '#ffffff', + bgcolor: '#ffeecc', } /** @@ -53,12 +53,25 @@ class RestoreDOMExample extends React.Component { render() { return (
+ +
    +
  1. + Click a brush to change color in state. Use refresh button to + `restoreDOM` which renders changes. +
  2. +
  3. + Press `!` button to corrupt DOM by removing `bold`. Backspace from + start of `text` 5 times. Console will show error but Slate will + recover by restoring DOM. +
  4. +
+
- {this.renderHighlightButton('#ffffff')} {this.renderHighlightButton('#ffeecc')} {this.renderHighlightButton('#ffffcc')} {this.renderHighlightButton('#ccffcc')} - {this.renderHighlightButton('#ccffff')} + {this.renderCorruptButton()} + {this.renderRestoreButton()} +
) } @@ -93,6 +108,48 @@ class RestoreDOMExample extends React.Component { ) } + /** + * Render restoreDOM button + */ + + renderRestoreButton = () => { + const { editor } = this + + function restoreDOM() { + editor.restoreDOM() + } + + return ( + + ) + } + + /** + * Render a button to corrupt the DOM + * + *@return {Element} + */ + + renderCorruptButton = () => { + /** + * Corrupt the DOM by forcibly deleting the first instance we can find + * of the `bold` text in the DOM. + */ + + function corrupt() { + const boldEl = window.document.querySelector('[data-bold]') + const el = boldEl.closest('[data-slate-object="text"]') + el.parentNode.removeChild(el) + } + return ( + + ) + } + /** * Highlight every block with a given background color * @@ -100,9 +157,7 @@ class RestoreDOMExample extends React.Component { */ onClickHighlight = bgcolor => { - const { editor } = this this.setState({ bgcolor }) - editor.restoreDOM() } /** @@ -128,6 +183,29 @@ class RestoreDOMExample extends React.Component { } } + /** + * Render a Slate mark. + * + * @param {Object} props + * @return {Element} + */ + + renderMark = (props, editor, next) => { + const { children, mark, attributes } = props + + switch (mark.type) { + case 'bold': + // Added `data-bold` so we can find bold text with `querySelector` + return ( + + {children} + + ) + default: + return next() + } + } + /** * On change, save the new `value`. * diff --git a/examples/restore-dom/value.json b/examples/restore-dom/value.json index 5011dacdd..64a4a8e58 100644 --- a/examples/restore-dom/value.json +++ b/examples/restore-dom/value.json @@ -9,7 +9,16 @@ "nodes": [ { "object": "text", - "text": "First block of text" + "text": "First block with " + }, + { + "object": "text", + "text": "bold", + "marks": [{ "type": "bold" }] + }, + { + "object": "text", + "text": " text in it" } ] }, @@ -19,7 +28,16 @@ "nodes": [ { "object": "text", - "text": "Second block of text" + "text": "Second block with " + }, + { + "object": "text", + "text": "bold", + "marks": [{ "type": "bold" }] + }, + { + "object": "text", + "text": " text in it" } ] }, @@ -29,7 +47,16 @@ "nodes": [ { "object": "text", - "text": "Third block of text" + "text": "Third block with " + }, + { + "object": "text", + "text": "bold", + "marks": [{ "type": "bold" }] + }, + { + "object": "text", + "text": " text in it" } ] } diff --git a/packages/slate-react/src/components/content.js b/packages/slate-react/src/components/content.js index 7c0cb8098..2d150f4c9 100644 --- a/packages/slate-react/src/components/content.js +++ b/packages/slate-react/src/components/content.js @@ -75,6 +75,22 @@ class Content extends React.Component { tagName: 'div', } + /** + * An error boundary. If there is a render error, we increment `errorKey` + * which is part of the container `key` which forces a re-render from + * scratch. + * + * @param {Error} error + * @param {String} info + */ + + componentDidCatch(error, info) { + debug('componentDidCatch', { error, info }) + // The call to `setState` is required despite not setting a value. + // Without this call, React will not try to recreate the component tree. + this.setState({}) + } + /** * Temporary values. *