From 4321539a41051d11e31730aca542988255e7eca6 Mon Sep 17 00:00:00 2001 From: George Treviranus Date: Tue, 27 Sep 2022 21:49:19 -0500 Subject: [PATCH 01/17] fix: fixes #6563 - performance improvements to pages with lists --- .../src/ListControl.js | 58 ++++++++++--------- .../src/ObjectControl.js | 20 ++++--- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/packages/netlify-cms-widget-list/src/ListControl.js b/packages/netlify-cms-widget-list/src/ListControl.js index e4c0459590ef..9cf30d386750 100644 --- a/packages/netlify-cms-widget-list/src/ListControl.js +++ b/packages/netlify-cms-widget-list/src/ListControl.js @@ -550,34 +550,36 @@ export default class ListControl extends React.Component { {this.objectLabel(item)} - - {({ css, cx }) => ( - - )} - + {!collapsed && ( + + {({ css, cx }) => ( + + )} + + )} ); }; diff --git a/packages/netlify-cms-widget-object/src/ObjectControl.js b/packages/netlify-cms-widget-object/src/ObjectControl.js index bf9dc593b056..5fc2357a6284 100644 --- a/packages/netlify-cms-widget-object/src/ObjectControl.js +++ b/packages/netlify-cms-widget-object/src/ObjectControl.js @@ -175,15 +175,17 @@ export default class ObjectControl extends React.Component { t={t} /> )} -
- {this.renderFields(multiFields, singleField)} -
+ {!collapsed && ( +
+ {this.renderFields(multiFields, singleField)} +
+ )} )} From c71d55694e0fce9b5721a7d832634f0b3b8110cb Mon Sep 17 00:00:00 2001 From: George Treviranus Date: Thu, 29 Sep 2022 20:02:29 -0500 Subject: [PATCH 02/17] test: updates list widget tests to check null collapse section --- .../src/__tests__/ListControl.spec.js | 75 ++++++++++--------- .../__snapshots__/ListControl.spec.js.snap | 55 -------------- 2 files changed, 38 insertions(+), 92 deletions(-) diff --git a/packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js b/packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js index 360958b252d3..150b84e2ef40 100644 --- a/packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js +++ b/packages/netlify-cms-widget-list/src/__tests__/ListControl.spec.js @@ -69,6 +69,7 @@ describe('ListControl', () => { return id++; }); }); + it('should render empty list', () => { const field = fromJS({ name: 'list', label: 'List' }); const { asFragment } = render(); @@ -96,7 +97,7 @@ describe('ListControl', () => { fields: [{ name: 'title', widget: 'string', label: 'Title' }], }, }); - const { asFragment, getByTestId } = render( + const { asFragment, getByTestId, queryByTestId } = render( { expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + expect(queryByTestId('object-control-0')).toBeNull(); + expect(queryByTestId('object-control-1')).toBeNull(); expect(asFragment()).toMatchSnapshot(); }); @@ -125,7 +126,7 @@ describe('ListControl', () => { fields: [{ name: 'title', widget: 'string', label: 'Title' }], }, }); - const { asFragment, getByTestId } = render( + const { asFragment, getByTestId, queryByTestId } = render( { expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + expect(queryByTestId('object-control-0')).not.toBeNull(); + expect(queryByTestId('object-control-1')).not.toBeNull(); expect(asFragment()).toMatchSnapshot(); }); @@ -154,7 +155,7 @@ describe('ListControl', () => { fields: [{ name: 'title', widget: 'string', label: 'Title' }], }, }); - const { getByTestId } = render( + const { getByTestId, queryByTestId } = render( { expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + expect(queryByTestId('object-control-0')).not.toBeNull(); + expect(queryByTestId('object-control-1')).not.toBeNull(); fireEvent.click(getByTestId('expand-button')); expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + expect(queryByTestId('object-control-0')).toBeNull(); + expect(queryByTestId('object-control-1')).toBeNull(); }); it('should collapse a single item on collapse item click', () => { @@ -189,7 +190,7 @@ describe('ListControl', () => { fields: [{ name: 'title', widget: 'string', label: 'Title' }], }, }); - const { getByTestId } = render( + const { getByTestId, queryByTestId } = render( { expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + expect(queryByTestId('object-control-0')).not.toBeNull(); + expect(queryByTestId('object-control-1')).not.toBeNull(); fireEvent.click(getByTestId('styled-list-item-top-bar-0')); expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + expect(queryByTestId('object-control-0')).toBeNull(); + expect(queryByTestId('object-control-1')).not.toBeNull(); }); it('should expand all items on top bar expand click', () => { @@ -224,7 +225,7 @@ describe('ListControl', () => { fields: [{ name: 'title', widget: 'string', label: 'Title' }], }, }); - const { getByTestId } = render( + const { getByTestId, queryByTestId } = render( { expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + expect(queryByTestId('object-control-0')).toBeNull(); + expect(queryByTestId('object-control-1')).toBeNull(); fireEvent.click(getByTestId('expand-button')); expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + expect(queryByTestId('object-control-0')).not.toBeNull(); + expect(queryByTestId('object-control-1')).not.toBeNull(); }); it('should expand a single item on expand item click', () => { @@ -259,7 +260,7 @@ describe('ListControl', () => { fields: [{ name: 'title', widget: 'string', label: 'Title' }], }, }); - const { getByTestId } = render( + const { getByTestId, queryByTestId } = render( { expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + expect(queryByTestId('object-control-0')).toBeNull(); + expect(queryByTestId('object-control-1')).toBeNull(); fireEvent.click(getByTestId('styled-list-item-top-bar-0')); expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + expect(queryByTestId('object-control-0')).not.toBeNull(); + expect(queryByTestId('object-control-1')).toBeNull(); }); it('should use widget name when no summary or label are configured for mixed types', () => { @@ -469,7 +470,7 @@ describe('ListControl', () => { label: 'List', fields: [{ label: 'String', name: 'string', widget: 'string' }], }); - const { asFragment, getByTestId } = render( + const { asFragment, getByTestId, queryByTestId } = render( { expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + expect(queryByTestId('object-control-0')).toBeNull(); + expect(queryByTestId('object-control-0')).toBeNull(); expect(asFragment()).toMatchSnapshot(); }); @@ -493,7 +494,7 @@ describe('ListControl', () => { collapsed: false, fields: [{ label: 'String', name: 'string', widget: 'string' }], }); - const { asFragment, getByTestId } = render( + const { asFragment, getByTestId, queryByTestId } = render( { expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + expect(queryByTestId('object-control-0')).not.toBeNull(); + expect(queryByTestId('object-control-1')).not.toBeNull(); expect(asFragment()).toMatchSnapshot(); }); @@ -538,8 +539,8 @@ describe('ListControl', () => { expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'true'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'true'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'true'); + expect(queryByTestId('object-control-0')).toBeNull(); + expect(queryByTestId('object-control-1')).toBeNull(); }); it('should render list with fields with collapse = "false" and default minimize_collapsed = "true"', () => { @@ -561,8 +562,8 @@ describe('ListControl', () => { expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); expect(getByTestId('styled-list-item-top-bar-1')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-1')).toHaveAttribute('collapsed', 'false'); + expect(queryByTestId('object-control-0')).not.toBeNull(); + expect(queryByTestId('object-control-1')).not.toBeNull(); expect(asFragment()).toMatchSnapshot(); @@ -595,7 +596,7 @@ describe('ListControl', () => { rerender(); expect(getByTestId('styled-list-item-top-bar-0')).toHaveAttribute('collapsed', 'false'); - expect(getByTestId('object-control-0')).toHaveAttribute('collapsed', 'false'); + expect(queryByTestId('object-control-0')).not.toBeNull(); expect(asFragment()).toMatchSnapshot(); }); diff --git a/packages/netlify-cms-widget-list/src/__tests__/__snapshots__/ListControl.spec.js.snap b/packages/netlify-cms-widget-list/src/__tests__/__snapshots__/ListControl.spec.js.snap index 069e7a49d70d..949ab97e7625 100644 --- a/packages/netlify-cms-widget-list/src/__tests__/__snapshots__/ListControl.spec.js.snap +++ b/packages/netlify-cms-widget-list/src/__tests__/__snapshots__/ListControl.spec.js.snap @@ -700,17 +700,6 @@ exports[`ListControl should remove from list when remove button is clicked 2`] = > item 2 - @@ -1457,17 +1446,6 @@ exports[`ListControl should render list with fields with default collapse ("true > item 1 -
item 2
- @@ -1897,17 +1864,6 @@ exports[`ListControl should render list with nested object 1`] = ` > Object -
Object
- From 24f92c16d3fbc4ad21882d390c27ee3ae50acc70 Mon Sep 17 00:00:00 2001 From: George Treviranus Date: Thu, 29 Sep 2022 20:20:58 -0500 Subject: [PATCH 03/17] refactor: remove unneeded display styles from list control's nested object control --- packages/netlify-cms-widget-list/src/ListControl.js | 12 ++---------- .../__tests__/__snapshots__/ListControl.spec.js.snap | 9 --------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/netlify-cms-widget-list/src/ListControl.js b/packages/netlify-cms-widget-list/src/ListControl.js index 9cf30d386750..ddc389276a54 100644 --- a/packages/netlify-cms-widget-list/src/ListControl.js +++ b/packages/netlify-cms-widget-list/src/ListControl.js @@ -44,9 +44,6 @@ const NestedObjectLabel = styled.div` `; const styleStrings = { - collapsedObjectControl: ` - display: none; - `, objectWidgetTopBarContainer: ` padding: ${lengths.objectWidgetTopBarContainerPadding}; `, @@ -552,13 +549,9 @@ export default class ListControl extends React.Component { {!collapsed && ( - {({ css, cx }) => ( + {({ cx }) => ( Date: Thu, 29 Sep 2022 21:28:06 -0500 Subject: [PATCH 04/17] refactor: remove Object widget's unneeded display styles --- .../src/ObjectControl.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/netlify-cms-widget-object/src/ObjectControl.js b/packages/netlify-cms-widget-object/src/ObjectControl.js index 5fc2357a6284..76dcbfd9e464 100644 --- a/packages/netlify-cms-widget-object/src/ObjectControl.js +++ b/packages/netlify-cms-widget-object/src/ObjectControl.js @@ -16,9 +16,6 @@ const styleStrings = { objectWidgetTopBarContainer: ` padding: ${lengths.objectWidgetTopBarContainerPadding}; `, - collapsedObjectControl: ` - display: none; - `, }; export default class ObjectControl extends React.Component { @@ -175,17 +172,7 @@ export default class ObjectControl extends React.Component { t={t} /> )} - {!collapsed && ( -
- {this.renderFields(multiFields, singleField)} -
- )} + {!collapsed &&
{this.renderFields(multiFields, singleField)}
} )}
From 7cfea013bf4730626b8194fa4d2440511e8751cb Mon Sep 17 00:00:00 2001 From: George Treviranus Date: Sun, 9 Oct 2022 20:42:02 -0500 Subject: [PATCH 05/17] refactor: debounce input values when updating redux store / document content; improved perf ftw --- .../src/CodeControl.js | 10 +++---- .../src/ColorControl.js | 27 +++++++++++++++---- .../src/DateTimeControl.js | 15 ++++++++--- .../src/MarkdownControl/RawEditor.js | 2 +- .../src/MarkdownControl/VisualEditor.js | 2 +- .../src/NumberControl.js | 17 ++++++++---- .../src/StringControl.js | 13 ++++++--- .../src/TextControl.js | 22 ++++++++++----- 8 files changed, 77 insertions(+), 31 deletions(-) diff --git a/packages/netlify-cms-widget-code/src/CodeControl.js b/packages/netlify-cms-widget-code/src/CodeControl.js index 20a690ff4f3e..0c808a0e1c8d 100644 --- a/packages/netlify-cms-widget-code/src/CodeControl.js +++ b/packages/netlify-cms-widget-code/src/CodeControl.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { ClassNames } from '@emotion/core'; import { Map } from 'immutable'; -import { uniq, isEqual, isEmpty } from 'lodash'; +import { uniq, isEqual, isEmpty, debounce } from 'lodash'; import uuid from 'uuid/v4'; import { UnControlled as ReactCodeMirror } from 'react-codemirror2'; import CodeMirror from 'codemirror'; @@ -99,6 +99,8 @@ export default class CodeControl extends React.Component { this.updateCodeMirrorProps(prevState); } + debounceOnChange = debounce(value => this.props.onChange(value), 300); + updateCodeMirrorProps(prevState) { const keys = ['lang', 'theme', 'keyMap']; const changedProps = getChangedProps(prevState, this.state, keys); @@ -165,8 +167,6 @@ export default class CodeControl extends React.Component { } async handleChangeCodeMirrorProps(changedProps) { - const { onChange } = this.props; - if (changedProps.lang) { const { mode } = this.getLanguageByName(changedProps.lang) || {}; if (mode) { @@ -195,7 +195,7 @@ export default class CodeControl extends React.Component { // Only persist the language change if supported - requires the value to be // a map rather than just a code string. if (changedProps.lang && this.valueIsMap()) { - onChange(this.toValue('lang', changedProps.lang)); + this.debounceOnChange(this.toValue('lang', changedProps.lang)); } } @@ -203,7 +203,7 @@ export default class CodeControl extends React.Component { const cursor = this.cm.doc.getCursor(); const selections = this.cm.doc.listSelections(); this.setState({ lastKnownValue: newValue }); - this.props.onChange(this.toValue('code', newValue), { cursor, selections }); + this.debounceOnChange(this.toValue('code', newValue), { cursor, selections }); } showSettings = () => { diff --git a/packages/netlify-cms-widget-colorstring/src/ColorControl.js b/packages/netlify-cms-widget-colorstring/src/ColorControl.js index 78ed2bf92e23..dedcd9d3325b 100644 --- a/packages/netlify-cms-widget-colorstring/src/ColorControl.js +++ b/packages/netlify-cms-widget-colorstring/src/ColorControl.js @@ -4,6 +4,7 @@ import styled from '@emotion/styled'; import ChromePicker from 'react-color'; import validateColor from 'validate-color'; import { zIndex } from 'netlify-cms-ui-default'; +import { debounce } from 'lodash'; function ClearIcon() { return ( @@ -91,28 +92,44 @@ export default class ColorControl extends React.Component { }; state = { + value: this.props.value, showColorPicker: false, }; + + debounceOnChange = debounce(value => this.props.onChange(value), 300); + // show/hide color picker handleClick = () => { this.setState({ showColorPicker: !this.state.showColorPicker }); }; + handleClear = () => { - this.props.onChange(''); + this.setState({ value: '' }); + this.debounceOnChange(''); }; + handleClose = () => { this.setState({ showColorPicker: false }); }; + + handleInputChange = e => { + const { value } = e.target; + this.setState({ value }); + this.debounceOnChange(value); + }; + handleChange = color => { const formattedColor = color.rgb.a < 1 ? `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})` : color.hex; - this.props.onChange(formattedColor); + this.setState({ value: formattedColor }); + this.debounceOnChange(formattedColor); }; + render() { - const { forID, value, field, onChange, classNameWrapper, setActiveStyle, setInactiveStyle } = - this.props; + const { forID, field, classNameWrapper, setActiveStyle, setInactiveStyle } = this.props; + const { value } = this.state; const allowInput = field.get('allowInput', false); @@ -153,7 +170,7 @@ export default class ColorControl extends React.Component { id={forID} className={classNameWrapper} value={value || ''} - onChange={e => onChange(e.target.value)} + onChange={this.handleInputChange} onFocus={setActiveStyle} onBlur={setInactiveStyle} style={{ diff --git a/packages/netlify-cms-widget-datetime/src/DateTimeControl.js b/packages/netlify-cms-widget-datetime/src/DateTimeControl.js index babebf0518eb..92e5808a07d1 100644 --- a/packages/netlify-cms-widget-datetime/src/DateTimeControl.js +++ b/packages/netlify-cms-widget-datetime/src/DateTimeControl.js @@ -6,6 +6,7 @@ import reactDateTimeStyles from 'react-datetime/css/react-datetime.css'; import DateTime from 'react-datetime'; import moment from 'moment'; import { buttons } from 'netlify-cms-ui-default'; +import { debounce } from 'lodash'; function NowButton({ t, handleChange }) { return ( @@ -44,6 +45,8 @@ export default class DateTimeControl extends React.Component { value: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), }; + state = { value: this.props.value }; + getFormats() { const { field } = this.props; const format = field.get('format'); @@ -99,6 +102,8 @@ export default class DateTimeControl extends React.Component { isValidDate = datetime => moment.isMoment(datetime) || datetime instanceof Date || datetime === ''; + debounceOnChange = debounce(value => this.props.onChange(value), 300); + handleChange = datetime => { /** * Set the date only if it is valid. @@ -107,7 +112,6 @@ export default class DateTimeControl extends React.Component { return; } - const { onChange } = this.props; const { format } = this.formats; /** @@ -116,10 +120,12 @@ export default class DateTimeControl extends React.Component { */ if (format) { const formattedValue = datetime ? moment(datetime).format(format) : ''; - onChange(formattedValue); + this.setState({ value: formattedValue }); + this.debounceOnChange(formattedValue); } else { const value = moment.isMoment(datetime) ? datetime.toDate() : datetime; - onChange(value); + this.setState({ value }); + this.debounceOnChange(value); } }; @@ -140,7 +146,8 @@ export default class DateTimeControl extends React.Component { }; render() { - const { forID, value, classNameWrapper, setActiveStyle, t, isDisabled } = this.props; + const { forID, classNameWrapper, setActiveStyle, t, isDisabled } = this.props; + const { value } = this.state; const { format, dateFormat, timeFormat } = this.formats; return ( diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/RawEditor.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/RawEditor.js index 6cf845276923..ab14b2cda59b 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/RawEditor.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/RawEditor.js @@ -99,7 +99,7 @@ export default class RawEditor extends React.Component { handleDocumentChange = debounce(editor => { const value = Plain.serialize(editor.value); this.props.onChange(value); - }, 150); + }, 300); handleToggleMode = () => { this.props.onMode('rich_text'); diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js index d3459d61b53e..70b2a27c9d0d 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js @@ -202,7 +202,7 @@ export default class Editor extends React.Component { remarkPlugins: this.remarkPlugins, }); onChange(markdown); - }, 150); + }, 300); handleChange = editor => { if (!this.state.value.document.equals(editor.value.document)) { diff --git a/packages/netlify-cms-widget-number/src/NumberControl.js b/packages/netlify-cms-widget-number/src/NumberControl.js index 35606d9e4211..d4f219216c5c 100644 --- a/packages/netlify-cms-widget-number/src/NumberControl.js +++ b/packages/netlify-cms-widget-number/src/NumberControl.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import { debounce } from 'lodash'; const ValidationErrorTypes = { PRESENCE: 'PRESENCE', PATTERN: 'PATTERN', @@ -68,15 +69,20 @@ export default class NumberControl extends React.Component { value: '', }; + state = { value: this.props.value }; + + debounceOnChange = debounce(value => this.props.onChange(value), 300); + handleChange = e => { const valueType = this.props.field.get('value_type'); - const { onChange } = this.props; const value = valueType === 'float' ? parseFloat(e.target.value) : parseInt(e.target.value, 10); - if (!isNaN(value)) { - onChange(value); + if (isNaN(value)) { + this.setState({ value: '' }); + this.debounceOnChange(''); } else { - onChange(''); + this.setState({ value }); + this.debounceOnChange(value); } }; @@ -96,7 +102,8 @@ export default class NumberControl extends React.Component { }; render() { - const { field, value, classNameWrapper, forID, setActiveStyle, setInactiveStyle } = this.props; + const { field, classNameWrapper, forID, setActiveStyle, setInactiveStyle } = this.props; + const { value } = this.state; const min = field.get('min', ''); const max = field.get('max', ''); const step = field.get('step', field.get('value_type') === 'int' ? 1 : ''); diff --git a/packages/netlify-cms-widget-string/src/StringControl.js b/packages/netlify-cms-widget-string/src/StringControl.js index bcb8dd14c2b0..b44b70fd809f 100644 --- a/packages/netlify-cms-widget-string/src/StringControl.js +++ b/packages/netlify-cms-widget-string/src/StringControl.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { debounce } from 'lodash'; export default class StringControl extends React.Component { static propTypes = { @@ -15,6 +16,10 @@ export default class StringControl extends React.Component { value: '', }; + state = { value: this.props.value }; + + debounceOnChange = debounce(value => this.props.onChange(value), 300); + // The selection to maintain for the input element _sel = 0; @@ -37,11 +42,13 @@ export default class StringControl extends React.Component { handleChange = e => { this._sel = e.target.selectionStart; - this.props.onChange(e.target.value); + const { value } = e.target; + this.setState({ value }); + this.debounceOnChange(value); }; render() { - const { forID, value, classNameWrapper, setActiveStyle, setInactiveStyle } = this.props; + const { forID, classNameWrapper, setActiveStyle, setInactiveStyle } = this.props; return ( this.props.onChange(value), 300); /** * Always update to ensure `react-textarea-autosize` properly calculates @@ -27,20 +30,25 @@ export default class TextControl extends React.Component { return true; } + handleChange = e => { + const { value } = e.target; + this.setState({ value }); + this.debounceOnChange(value); + }; + render() { - const { forID, value, onChange, classNameWrapper, setActiveStyle, setInactiveStyle } = - this.props; + const { forID, classNameWrapper, setActiveStyle, setInactiveStyle } = this.props; return (