diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js index b9d4e54b14..800612dba1 100644 --- a/src/components/fields/ArrayField.js +++ b/src/components/fields/ArrayField.js @@ -12,8 +12,7 @@ import { retrieveSchema, toIdSchema, shouldRender, - getDefaultRegistry, - setState + getDefaultRegistry } from "../../utils"; function ArrayFieldTitle({TitleField, idSchema, title, required}) { @@ -151,6 +150,7 @@ function DefaultNormalArrayFieldTemplate(props) { class ArrayField extends Component { static defaultProps = { uiSchema: {}, + formData: [], idSchema: {}, registry: getDefaultRegistry(), required: false, @@ -159,23 +159,6 @@ class ArrayField extends Component { autofocus: false, }; - constructor(props) { - super(props); - this.state = this.getStateFromProps(props); - } - - componentWillReceiveProps(nextProps) { - this.setState(this.getStateFromProps(nextProps)); - } - - getStateFromProps(props) { - const formData = Array.isArray(props.formData) ? props.formData : null; - const {definitions} = this.props.registry; - return { - items: getDefaultFormState(props.schema, formData, definitions) || [] - }; - } - shouldComponentUpdate(nextProps, nextState) { return shouldRender(this, nextProps, nextState); } @@ -189,26 +172,18 @@ class ArrayField extends Component { return itemsSchema.type === "string" && itemsSchema.minLength > 0; } - asyncSetState(state, options={validate: false}) { - setState(this, state, () => { - this.props.onChange(this.state.items, options); - }); - } - onAddClick = (event) => { event.preventDefault(); - const {items} = this.state; - const {schema, registry} = this.props; + const {schema, registry, formData} = this.props; const {definitions} = registry; let itemSchema = schema.items; if (isFixedItems(schema) && allowAdditionalItems(schema)) { itemSchema = schema.additionalItems; } - this.asyncSetState({ - items: items.concat([ - getDefaultFormState(itemSchema, undefined, definitions) - ]) - }); + this.props.onChange([ + ...formData, + getDefaultFormState(itemSchema, undefined, definitions) + ], {validate: false}); }; onDropIndexClick = (index) => { @@ -216,9 +191,10 @@ class ArrayField extends Component { if (event) { event.preventDefault(); } - this.asyncSetState({ - items: this.state.items.filter((_, i) => i !== index) - }, {validate: true}); // refs #195 + this.props.onChange( + this.props.formData.filter((_, i) => i !== index), + {validate: true} // refs #195 + ); }; }; @@ -228,9 +204,9 @@ class ArrayField extends Component { event.preventDefault(); event.target.blur(); } - const {items} = this.state; - this.asyncSetState({ - items: items.map((item, i) => { + const items = this.props.formData; + this.props.onChange( + items.map((item, i) => { if (i === newIndex) { return items[index]; } else if (i === index) { @@ -238,23 +214,25 @@ class ArrayField extends Component { } else { return item; } - }) - }, {validate: true}); + }), + {validate: true} + ); }; }; onChangeForIndex = (index) => { return (value) => { - this.asyncSetState({ - items: this.state.items.map((item, i) => { + this.props.onChange( + this.props.formData.map((item, i) => { return index === i ? value : item; - }) - }); + }), + {validate: false} + ); }; }; onSelectChange = (value) => { - this.asyncSetState({items: value}); + this.props.onChange(value, {validate: false}); }; render() { @@ -275,6 +253,7 @@ class ArrayField extends Component { const { schema, uiSchema, + formData, errorSchema, idSchema, name, @@ -287,7 +266,6 @@ class ArrayField extends Component { onBlur } = this.props; const title = (schema.title === undefined) ? name : schema.title; - const {items = []} = this.state; const {ArrayFieldTemplate, definitions, fields} = registry; const {TitleField, DescriptionField} = fields; const itemsSchema = retrieveSchema(schema.items, definitions); @@ -295,18 +273,18 @@ class ArrayField extends Component { const arrayProps = { canAdd: addable, - items: items.map((item, index) => { + items: formData.map((item, index) => { const itemErrorSchema = errorSchema ? errorSchema[index] : undefined; const itemIdPrefix = idSchema.$id + "_" + index; const itemIdSchema = toIdSchema(itemsSchema, itemIdPrefix, definitions); return this.renderArrayFieldItem({ index, canMoveUp: index > 0, - canMoveDown: index < items.length - 1, + canMoveDown: index < formData.length - 1, itemSchema: itemsSchema, itemIdSchema, itemErrorSchema, - itemData: items[index], + itemData: formData[index], itemUiSchema: uiSchema.items, autofocus: autofocus && index === 0, onBlur @@ -332,7 +310,7 @@ class ArrayField extends Component { renderMultiSelect() { const {schema, idSchema, uiSchema, disabled, readonly, autofocus, onBlur} = this.props; - const {items} = this.state; + const items = this.props.formData; const {widgets, definitions, formContext} = this.props.registry; const itemsSchema = retrieveSchema(schema.items, definitions); const enumOptions = optionsList(itemsSchema); @@ -357,7 +335,7 @@ class ArrayField extends Component { renderFiles() { const {schema, uiSchema, idSchema, name, disabled, readonly, autofocus, onBlur} = this.props; const title = schema.title || name; - const {items} = this.state; + const items = this.props.formData; const {widgets, formContext} = this.props.registry; const {widget="files", ...options} = getUiOptions(uiSchema); const Widget = getWidget(schema, widget, widgets); @@ -393,7 +371,7 @@ class ArrayField extends Component { onBlur } = this.props; const title = schema.title || name; - let {items} = this.state; + let items = this.props.formData; const {ArrayFieldTemplate, definitions, fields} = registry; const {TitleField} = fields; const itemSchemas = schema.items.map(item => diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index 1512aa1dbc..97bb12966a 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -1,35 +1,16 @@ import React, {Component, PropTypes} from "react"; -import {deepEquals} from "../../utils"; - - import { - getDefaultFormState, orderProperties, retrieveSchema, shouldRender, - getDefaultRegistry, - setState + getDefaultRegistry } from "../../utils"; - -function objectKeysHaveChanged(formData, state) { - // for performance, first check for lengths - const newKeys = Object.keys(formData); - const oldKeys = Object.keys(state); - if (newKeys.length < oldKeys.length) { - return true; - } - // deep check on sorted keys - if (!deepEquals(newKeys.sort(), oldKeys.sort())) { - return true; - } - return false; -} - class ObjectField extends Component { static defaultProps = { uiSchema: {}, + formData: {}, errorSchema: {}, idSchema: {}, registry: getDefaultRegistry(), @@ -38,29 +19,6 @@ class ObjectField extends Component { readonly: false, } - constructor(props) { - super(props); - this.state = this.getStateFromProps(props); - } - - componentWillReceiveProps(nextProps) { - const state = this.getStateFromProps(nextProps); - const {formData} = nextProps; - if (formData && objectKeysHaveChanged(formData, this.state)) { - // We *need* to replace state entirely here has we have received formData - // holding different keys (so with some removed). - this.state = state; - this.forceUpdate(); - } else { - this.setState(state); - } - } - - getStateFromProps(props) { - const {schema, formData, registry} = props; - return getDefaultFormState(schema, formData, registry.definitions) || {}; - } - shouldComponentUpdate(nextProps, nextState) { return shouldRender(this, nextProps, nextState); } @@ -71,21 +29,17 @@ class ObjectField extends Component { schema.required.indexOf(name) !== -1; } - asyncSetState(state, options={validate: false}) { - setState(this, state, () => { - this.props.onChange(this.state, options); - }); - } - onPropertyChange = (name) => { return (value, options) => { - this.asyncSetState({[name]: value}, options); + const newFormData = {...this.props.formData, [name]: value}; + this.props.onChange(newFormData, options); }; }; render() { const { uiSchema, + formData, errorSchema, idSchema, name, @@ -135,7 +89,7 @@ class ObjectField extends Component { uiSchema={uiSchema[name]} errorSchema={errorSchema[name]} idSchema={idSchema[name]} - formData={this.state[name]} + formData={formData[name]} onChange={this.onPropertyChange(name)} onBlur={onBlur} registry={this.props.registry} diff --git a/test/performance_test.js b/test/performance_test.js index 4816982178..bb4f5d8277 100644 --- a/test/performance_test.js +++ b/test/performance_test.js @@ -7,7 +7,8 @@ import ObjectField from "../src/components/fields/ObjectField"; import { createComponent, createFormComponent, - createSandbox + createSandbox, + setProps } from "./test_utils"; @@ -56,33 +57,35 @@ describe("Rendering performance optimizations", () => { const registry = getDefaultRegistry(); it("should not render if next props are equivalent", () => { - const {comp} = createComponent(ArrayField, { + const props = { registry, schema, uiSchema, onChange, onBlur - }); + }; + + const {comp} = createComponent(ArrayField, props); sandbox.stub(comp, "render").returns(
); - comp.componentWillReceiveProps({schema}); + setProps(comp, props); sinon.assert.notCalled(comp.render); }); it("should not render if next formData are equivalent", () => { - const formData = ["a", "b"]; - - const {comp} = createComponent(ArrayField, { + const props = { registry, schema, - formData, + formData: ["a", "b"], onChange, onBlur - }); + }; + + const {comp} = createComponent(ArrayField, props); sandbox.stub(comp, "render").returns(
); - comp.componentWillReceiveProps({schema, formData}); + setProps(comp, {...props, formData: ["a", "b"]}); sinon.assert.notCalled(comp.render); }); @@ -102,35 +105,37 @@ describe("Rendering performance optimizations", () => { const idSchema = {$id: "root", foo: {$id: "root_plop"}}; it("should not render if next props are equivalent", () => { - const {comp} = createComponent(ObjectField, { + const props = { registry, schema, uiSchema, onChange, idSchema, onBlur - }); + }; + + const {comp} = createComponent(ObjectField, props); sandbox.stub(comp, "render").returns(
); - comp.componentWillReceiveProps({schema, registry}); + setProps(comp, props); sinon.assert.notCalled(comp.render); }); it("should not render if next formData are equivalent", () => { - const formData = {foo: "blah"}; - - const {comp} = createComponent(ObjectField, { + const props = { registry, schema, - formData, + formData: {foo: "blah"}, onChange, idSchema, onBlur - }); + }; + + const {comp} = createComponent(ObjectField, props); sandbox.stub(comp, "render").returns(
); - comp.componentWillReceiveProps({schema, formData, registry}); + setProps(comp, {...props, formData: {foo: "blah"}}); sinon.assert.notCalled(comp.render); }); diff --git a/test/test_utils.js b/test/test_utils.js index c2ab0efa47..2a03e5d65e 100644 --- a/test/test_utils.js +++ b/test/test_utils.js @@ -3,7 +3,7 @@ import React from "react"; import sinon from "sinon"; import {renderIntoDocument} from "react-addons-test-utils"; -import {findDOMNode} from "react-dom"; +import {findDOMNode, render} from "react-dom"; import Form from "../src"; @@ -26,3 +26,13 @@ export function createSandbox() { }); return sandbox; } + +export function setProps(comp, newProps) { + const node = findDOMNode(comp); + render( + React.createElement( + comp.constructor, + newProps + ), + node.parentNode); +}