From d2a3415c16b6d721250760f7bc2b1726e9be4cd0 Mon Sep 17 00:00:00 2001 From: Dave Draper Date: Mon, 18 Feb 2019 10:48:33 +0000 Subject: [PATCH 1/5] Initial updates to add tests that cause the failure --- packages/core/src/components/Form.js | 7 + packages/core/src/components/Form.test.js | 522 +++++++++++----------- 2 files changed, 280 insertions(+), 249 deletions(-) diff --git a/packages/core/src/components/Form.js b/packages/core/src/components/Form.js index 29e5702..74fe6e0 100644 --- a/packages/core/src/components/Form.js +++ b/packages/core/src/components/Form.js @@ -123,6 +123,12 @@ export default class Form extends Component< fields = registerFields(fieldsFromState, value); } + // TODO: Merge value from fields into valueu? + fields.forEach(field => { + console.log("field >> ", field); + value[field.name] = field.value; + }); + // We should reset the touched state of all the fields if the value passed as a prop to the form // changes... const resetTouchedState = defaultValueChange; @@ -158,6 +164,7 @@ export default class Form extends Component< let { fields } = this.state; fields = updateFieldTouchedState(id, true, fields); fields = updateFieldValue(id, value, fields); + const nextState = getNextStateFromFields({ fields, lastFieldUpdated: id, diff --git a/packages/core/src/components/Form.test.js b/packages/core/src/components/Form.test.js index d7eeb0f..f5468b8 100644 --- a/packages/core/src/components/Form.test.js +++ b/packages/core/src/components/Form.test.js @@ -13,190 +13,285 @@ import type { FieldDef } from "../types"; chai.use(chaiEnzyme()); Enzyme.configure({ adapter: new Adapter() }); -describe("Context", () => { - const onFormChange = jest.fn(); - const singleField: FieldDef[] = [ - { - id: "FIELD1", - name: "prop1", - defaultValue: "test", - type: "text" - } - ]; - - test("provides value", () => { - const propValue = { prop1: "value1" }; - let value; - const form = mount( -
- - {context => { - expect(context.value).toEqual(propValue); - }} - -
- ); - expect(form.state().value).toEqual(propValue); - }); - - test("FormFragment has the correct value", () => { - const propValue = { prop1: "value1" }; - let value; - const form = mount( -
- - - ); - expect(form.state().value).toEqual(propValue); - }); - - test("FormFragment has the correct value after props update", () => { - const propValue = { prop1: "value1" }; - let value; - const form = mount( -
- - - ); - expect(form.state().value.prop1).toEqual("value1"); - - form.setProps({ value: { prop1: "value2" } }); - expect(form.state().value.prop1).toEqual("value2"); - }); -}); - -describe("validation warnings", () => { +// describe("Context", () => { +// const onFormChange = jest.fn(); +// const singleField: FieldDef[] = [ +// { +// id: "FIELD1", +// name: "prop1", +// defaultValue: "test", +// type: "text" +// } +// ]; + +// test("provides value", () => { +// const propValue = { prop1: "value1" }; +// let value; +// const form = mount( +//
+// +// {context => { +// expect(context.value).toEqual(propValue); +// }} +// +//
+// ); +// expect(form.state().value).toEqual(propValue); +// }); + +// test("FormFragment has the correct value", () => { +// const propValue = { prop1: "value1" }; +// let value; +// const form = mount( +//
+// +// +// ); +// expect(form.state().value).toEqual(propValue); +// }); + +// test("FormFragment has the correct value after props update", () => { +// const propValue = { prop1: "value1" }; +// let value; +// const form = mount( +//
+// +// +// ); +// expect(form.state().value.prop1).toEqual("value1"); + +// form.setProps({ value: { prop1: "value2" } }); +// expect(form.state().value.prop1).toEqual("value2"); +// }); +// }); + +// describe("validation warnings", () => { +// const fields: FieldDef[] = [ +// { +// id: "FIELD1", +// type: "text", +// name: "field1", +// validWhen: { +// matchesRegEx: { +// pattern: "^[\\d]+$", +// message: "Numbers only" +// } +// } +// } +// ]; + +// const onButtonClick = jest.fn(); +// const formValue = { +// field1: "test" +// }; + +// describe("shown immediately", () => { +// const form = mount( +//
+// +// +// +// ); + +// test("form state is invalid", () => { +// expect(form.state().isValid).toBe(false); +// }); + +// test("field should be invalid", () => { +// expect(form.state().fields[0].isValid).toBe(false); +// }); + +// test("warning is shown when field is invalid", () => { +// expect(form.find("span.errors").length).toBe(1); +// }); + +// test("warning is displayed as configured", () => { +// expect(form.find("span.errors").text()).toBe("Numbers only"); +// }); +// }); + +// describe("shown when touched", () => { +// const form = mount( +//
+// +// +// +// ); + +// test("form state is invalid", () => { +// expect(form.state().isValid).toBe(false); +// }); + +// test("field has not been touched", () => { +// expect(form.state().fields[0].touched).toBe(false); +// }); + +// test("field should be valid (as it's not been touched)", () => { +// expect(form.state().fields[0].isValid).toBe(true); +// }); + +// test("button should still be disabled", () => { +// chai.expect(form.find("button")).to.be.disabled(); +// }); + +// test("field should be discretely invalid (as it's not been touched)", () => { +// expect(form.state().fields[0].isDiscretelyInvalid).toBe(true); +// }); + +// test("warning is not shown as field has not been touched", () => { +// expect(form.find("span.errors").length).toBe(0); +// }); + +// test("field should NOT be touched after focus", () => { +// form.find("input").prop("onFocus")(); +// expect(form.state().fields[0].touched).toBe(false); +// }); + +// test("field should be touched after blur", () => { +// form.find("input").prop("onBlur")(); +// expect(form.state().fields[0].touched).toBe(true); +// }); + +// test("field should be invalid", () => { +// expect(form.state().fields[0].isValid).toBe(false); +// }); + +// test("field has correct error message", () => { +// expect(form.state().fields[0].errorMessages).toBe("Numbers only"); +// }); + +// test("warning now shown as field has been touched", () => { +// form.update(); +// expect(form.find("span.errors").length).toBe(1); +// }); + +// test("warning is displayed as configured", () => { +// expect(form.find("span.errors").text()).toBe("Numbers only"); +// }); +// }); +// }); + +// describe("changing form value prop", () => { +// const fields: FieldDef[] = [ +// { +// id: "FIELD1", +// type: "text", +// name: "field1", +// validWhen: { +// matchesRegEx: { +// pattern: "^[\\d]+$", +// message: "Numbers only" +// } +// } +// } +// ]; + +// const onButtonClick = jest.fn(); +// const formValue1 = { +// field1: "value1" +// }; +// const formValue2 = { +// field1: "value2" +// }; + +// const form = mount( +//
+// +// +// ); + +// test("field has not been touched", () => { +// expect(form.state().fields[0].touched).toBe(false); +// }); + +// test("field should NOT be touched after focus", () => { +// form.find("input").prop("onFocus")(); +// form.update(); +// expect(form.state().fields[0].touched).toBe(false); +// }); + +// test("field should be touched after blur", () => { +// form.find("input").prop("onBlur")(); +// form.update(); +// expect(form.state().fields[0].touched).toBe(true); +// }); + +// test("field should not be touched after form value update", () => { +// form.setProps({ value: formValue2 }); +// form.update(); +// expect(form.state().fields[0].touched).toBe(false); +// }); +// }); + +// describe("Form and FormFragment behave the same with defaultFields and value", () => { +// // This example is taken from issue: https://github.com/draperd/react-forms-processor/issues/35 +// const fruit: FieldDef[] = [ +// { +// id: "PICKMORETHANONE", +// name: "fruit", +// label: "Pick some fruit", +// placeholder: "Available fruits...", +// type: "multiselect", +// defaultValue: "apple,banana", +// valueDelimiter: ",", +// options: [ +// { +// items: ["apple", "banana", "kiwi", "melon", "grapefruit", "plum"] +// } +// ] +// } +// ]; + +// test("Form field has correct value from defaultFields", () => { +// const form = mount(
); +// const fieldValue = form.find("select").prop("value"); +// expect(fieldValue).toEqual(["apple", "banana"]); +// }); + +// test("FormFragment field has correct value from defaultFields", () => { +// const form = mount( +// +// +// +// ); +// const fieldValue = form.find("select").prop("value"); +// expect(fieldValue).toEqual(["apple", "banana"]); +// }); + +// test("Form field has correct value from value", () => { +// const form = mount( +//
+// ); +// const fieldValue = form.find("select").prop("value"); +// expect(fieldValue).toEqual(["melon"]); +// }); + +// test("FormFragment field has correct value from value", () => { +// const form = mount( +// +// +// +// ); +// const fieldValue = form.find("select").prop("value"); +// expect(fieldValue).toEqual(["melon"]); +// }); +// }); + +describe("Form with initial value updates value on field change", () => { const fields: FieldDef[] = [ { id: "FIELD1", type: "text", - name: "field1", - validWhen: { - matchesRegEx: { - pattern: "^[\\d]+$", - message: "Numbers only" - } - } + name: "field1" } ]; - - const onButtonClick = jest.fn(); - const formValue = { - field1: "test" - }; - - describe("shown immediately", () => { - const form = mount( -
- - - - ); - - test("form state is invalid", () => { - expect(form.state().isValid).toBe(false); - }); - - test("field should be invalid", () => { - expect(form.state().fields[0].isValid).toBe(false); - }); - - test("warning is shown when field is invalid", () => { - expect(form.find("span.errors").length).toBe(1); - }); - - test("warning is displayed as configured", () => { - expect(form.find("span.errors").text()).toBe("Numbers only"); - }); - }); - - describe("shown when touched", () => { - const form = mount( -
- - - - ); - - test("form state is invalid", () => { - expect(form.state().isValid).toBe(false); - }); - - test("field has not been touched", () => { - expect(form.state().fields[0].touched).toBe(false); - }); - - test("field should be valid (as it's not been touched)", () => { - expect(form.state().fields[0].isValid).toBe(true); - }); - - test("button should still be disabled", () => { - chai.expect(form.find("button")).to.be.disabled(); - }); - - test("field should be discretely invalid (as it's not been touched)", () => { - expect(form.state().fields[0].isDiscretelyInvalid).toBe(true); - }); - - test("warning is not shown as field has not been touched", () => { - expect(form.find("span.errors").length).toBe(0); - }); - - test("field should NOT be touched after focus", () => { - form.find("input").prop("onFocus")(); - expect(form.state().fields[0].touched).toBe(false); - }); - - test("field should be touched after blur", () => { - form.find("input").prop("onBlur")(); - expect(form.state().fields[0].touched).toBe(true); - }); - - test("field should be invalid", () => { - expect(form.state().fields[0].isValid).toBe(false); - }); - - test("field has correct error message", () => { - expect(form.state().fields[0].errorMessages).toBe("Numbers only"); - }); - - test("warning now shown as field has been touched", () => { - form.update(); - expect(form.find("span.errors").length).toBe(1); - }); - - test("warning is displayed as configured", () => { - expect(form.find("span.errors").text()).toBe("Numbers only"); - }); - }); -}); - -describe("changing form value prop", () => { - const fields: FieldDef[] = [ - { - id: "FIELD1", - type: "text", - name: "field1", - validWhen: { - matchesRegEx: { - pattern: "^[\\d]+$", - message: "Numbers only" - } - } - } - ]; - - const onButtonClick = jest.fn(); const formValue1 = { field1: "value1" }; - const formValue2 = { - field1: "value2" - }; const form = mount(
@@ -204,79 +299,8 @@ describe("changing form value prop", () => {
); - test("field has not been touched", () => { - expect(form.state().fields[0].touched).toBe(false); - }); - - test("field should NOT be touched after focus", () => { - form.find("input").prop("onFocus")(); - form.update(); - expect(form.state().fields[0].touched).toBe(false); - }); - - test("field should be touched after blur", () => { - form.find("input").prop("onBlur")(); - form.update(); - expect(form.state().fields[0].touched).toBe(true); - }); - - test("field should not be touched after form value update", () => { - form.setProps({ value: formValue2 }); - form.update(); - expect(form.state().fields[0].touched).toBe(false); - }); -}); - -describe("Form and FormFragment behave the same with defaultFields and value", () => { - // This example is taken from issue: https://github.com/draperd/react-forms-processor/issues/35 - const fruit: FieldDef[] = [ - { - id: "PICKMORETHANONE", - name: "fruit", - label: "Pick some fruit", - placeholder: "Available fruits...", - type: "multiselect", - defaultValue: "apple,banana", - valueDelimiter: ",", - options: [ - { - items: ["apple", "banana", "kiwi", "melon", "grapefruit", "plum"] - } - ] - } - ]; - - test("Form field has correct value from defaultFields", () => { - const form = mount(
); - const fieldValue = form.find("select").prop("value"); - expect(fieldValue).toEqual(["apple", "banana"]); - }); - - test("FormFragment field has correct value from defaultFields", () => { - const form = mount( - - - - ); - const fieldValue = form.find("select").prop("value"); - expect(fieldValue).toEqual(["apple", "banana"]); - }); - - test("Form field has correct value from value", () => { - const form = mount( -
- ); - const fieldValue = form.find("select").prop("value"); - expect(fieldValue).toEqual(["melon"]); - }); - - test("FormFragment field has correct value from value", () => { - const form = mount( - - - - ); - const fieldValue = form.find("select").prop("value"); - expect(fieldValue).toEqual(["melon"]); + test("moomin", () => { + form.find("input").prop("onChange")({ target: { value: "updated" } }); + expect(form.state().value.field1).toBe("updated"); }); }); From 7f623a542764abd6051f116ae50a6afcfff3daea Mon Sep 17 00:00:00 2001 From: Dave Draper Date: Mon, 18 Feb 2019 11:05:34 +0000 Subject: [PATCH 2/5] Better but a couple of tests still failing --- packages/core/src/components/Form.js | 15 +- packages/core/src/components/Form.test.js | 534 +++++++++++----------- 2 files changed, 276 insertions(+), 273 deletions(-) diff --git a/packages/core/src/components/Form.js b/packages/core/src/components/Form.js index 74fe6e0..9aec695 100644 --- a/packages/core/src/components/Form.js +++ b/packages/core/src/components/Form.js @@ -115,6 +115,15 @@ export default class Form extends Component< const value = valueFromProps || valueFromState || {}; const defaultFields = defaultFieldsFromProps || fieldsFromState; + + // Merge value from fields (if set) into form value... + // This is done to ensure the form value has the latest value defined... + defaultFields.forEach(field => { + if (field.value) { + value[field.name] = field.value; + } + }); + let fields; if (defaultFieldsFromProps && defaultFieldsChange) { fields = registerFields(defaultFieldsFromProps, value); @@ -123,12 +132,6 @@ export default class Form extends Component< fields = registerFields(fieldsFromState, value); } - // TODO: Merge value from fields into valueu? - fields.forEach(field => { - console.log("field >> ", field); - value[field.name] = field.value; - }); - // We should reset the touched state of all the fields if the value passed as a prop to the form // changes... const resetTouchedState = defaultValueChange; diff --git a/packages/core/src/components/Form.test.js b/packages/core/src/components/Form.test.js index f5468b8..4e47381 100644 --- a/packages/core/src/components/Form.test.js +++ b/packages/core/src/components/Form.test.js @@ -13,273 +13,273 @@ import type { FieldDef } from "../types"; chai.use(chaiEnzyme()); Enzyme.configure({ adapter: new Adapter() }); -// describe("Context", () => { -// const onFormChange = jest.fn(); -// const singleField: FieldDef[] = [ -// { -// id: "FIELD1", -// name: "prop1", -// defaultValue: "test", -// type: "text" -// } -// ]; - -// test("provides value", () => { -// const propValue = { prop1: "value1" }; -// let value; -// const form = mount( -//
-// -// {context => { -// expect(context.value).toEqual(propValue); -// }} -// -//
-// ); -// expect(form.state().value).toEqual(propValue); -// }); - -// test("FormFragment has the correct value", () => { -// const propValue = { prop1: "value1" }; -// let value; -// const form = mount( -//
-// -// -// ); -// expect(form.state().value).toEqual(propValue); -// }); - -// test("FormFragment has the correct value after props update", () => { -// const propValue = { prop1: "value1" }; -// let value; -// const form = mount( -//
-// -// -// ); -// expect(form.state().value.prop1).toEqual("value1"); - -// form.setProps({ value: { prop1: "value2" } }); -// expect(form.state().value.prop1).toEqual("value2"); -// }); -// }); - -// describe("validation warnings", () => { -// const fields: FieldDef[] = [ -// { -// id: "FIELD1", -// type: "text", -// name: "field1", -// validWhen: { -// matchesRegEx: { -// pattern: "^[\\d]+$", -// message: "Numbers only" -// } -// } -// } -// ]; - -// const onButtonClick = jest.fn(); -// const formValue = { -// field1: "test" -// }; - -// describe("shown immediately", () => { -// const form = mount( -//
-// -// -// -// ); - -// test("form state is invalid", () => { -// expect(form.state().isValid).toBe(false); -// }); - -// test("field should be invalid", () => { -// expect(form.state().fields[0].isValid).toBe(false); -// }); - -// test("warning is shown when field is invalid", () => { -// expect(form.find("span.errors").length).toBe(1); -// }); - -// test("warning is displayed as configured", () => { -// expect(form.find("span.errors").text()).toBe("Numbers only"); -// }); -// }); - -// describe("shown when touched", () => { -// const form = mount( -//
-// -// -// -// ); - -// test("form state is invalid", () => { -// expect(form.state().isValid).toBe(false); -// }); - -// test("field has not been touched", () => { -// expect(form.state().fields[0].touched).toBe(false); -// }); - -// test("field should be valid (as it's not been touched)", () => { -// expect(form.state().fields[0].isValid).toBe(true); -// }); - -// test("button should still be disabled", () => { -// chai.expect(form.find("button")).to.be.disabled(); -// }); - -// test("field should be discretely invalid (as it's not been touched)", () => { -// expect(form.state().fields[0].isDiscretelyInvalid).toBe(true); -// }); - -// test("warning is not shown as field has not been touched", () => { -// expect(form.find("span.errors").length).toBe(0); -// }); - -// test("field should NOT be touched after focus", () => { -// form.find("input").prop("onFocus")(); -// expect(form.state().fields[0].touched).toBe(false); -// }); - -// test("field should be touched after blur", () => { -// form.find("input").prop("onBlur")(); -// expect(form.state().fields[0].touched).toBe(true); -// }); - -// test("field should be invalid", () => { -// expect(form.state().fields[0].isValid).toBe(false); -// }); - -// test("field has correct error message", () => { -// expect(form.state().fields[0].errorMessages).toBe("Numbers only"); -// }); - -// test("warning now shown as field has been touched", () => { -// form.update(); -// expect(form.find("span.errors").length).toBe(1); -// }); - -// test("warning is displayed as configured", () => { -// expect(form.find("span.errors").text()).toBe("Numbers only"); -// }); -// }); -// }); - -// describe("changing form value prop", () => { -// const fields: FieldDef[] = [ -// { -// id: "FIELD1", -// type: "text", -// name: "field1", -// validWhen: { -// matchesRegEx: { -// pattern: "^[\\d]+$", -// message: "Numbers only" -// } -// } -// } -// ]; - -// const onButtonClick = jest.fn(); -// const formValue1 = { -// field1: "value1" -// }; -// const formValue2 = { -// field1: "value2" -// }; - -// const form = mount( -//
-// -// -// ); - -// test("field has not been touched", () => { -// expect(form.state().fields[0].touched).toBe(false); -// }); - -// test("field should NOT be touched after focus", () => { -// form.find("input").prop("onFocus")(); -// form.update(); -// expect(form.state().fields[0].touched).toBe(false); -// }); - -// test("field should be touched after blur", () => { -// form.find("input").prop("onBlur")(); -// form.update(); -// expect(form.state().fields[0].touched).toBe(true); -// }); - -// test("field should not be touched after form value update", () => { -// form.setProps({ value: formValue2 }); -// form.update(); -// expect(form.state().fields[0].touched).toBe(false); -// }); -// }); - -// describe("Form and FormFragment behave the same with defaultFields and value", () => { -// // This example is taken from issue: https://github.com/draperd/react-forms-processor/issues/35 -// const fruit: FieldDef[] = [ -// { -// id: "PICKMORETHANONE", -// name: "fruit", -// label: "Pick some fruit", -// placeholder: "Available fruits...", -// type: "multiselect", -// defaultValue: "apple,banana", -// valueDelimiter: ",", -// options: [ -// { -// items: ["apple", "banana", "kiwi", "melon", "grapefruit", "plum"] -// } -// ] -// } -// ]; - -// test("Form field has correct value from defaultFields", () => { -// const form = mount(
); -// const fieldValue = form.find("select").prop("value"); -// expect(fieldValue).toEqual(["apple", "banana"]); -// }); - -// test("FormFragment field has correct value from defaultFields", () => { -// const form = mount( -// -// -// -// ); -// const fieldValue = form.find("select").prop("value"); -// expect(fieldValue).toEqual(["apple", "banana"]); -// }); - -// test("Form field has correct value from value", () => { -// const form = mount( -//
-// ); -// const fieldValue = form.find("select").prop("value"); -// expect(fieldValue).toEqual(["melon"]); -// }); - -// test("FormFragment field has correct value from value", () => { -// const form = mount( -// -// -// -// ); -// const fieldValue = form.find("select").prop("value"); -// expect(fieldValue).toEqual(["melon"]); -// }); -// }); +describe("Context", () => { + const onFormChange = jest.fn(); + const singleField: FieldDef[] = [ + { + id: "FIELD1", + name: "prop1", + defaultValue: "test", + type: "text" + } + ]; + + test("provides value", () => { + const propValue = { prop1: "value1" }; + let value; + const form = mount( +
+ + {context => { + expect(context.value).toEqual(propValue); + }} + +
+ ); + expect(form.state().value).toEqual(propValue); + }); + + test("FormFragment has the correct value", () => { + const propValue = { prop1: "value1" }; + let value; + const form = mount( +
+ + + ); + expect(form.state().value).toEqual(propValue); + }); + + test("FormFragment has the correct value after props update", () => { + const propValue = { prop1: "value1" }; + let value; + const form = mount( +
+ + + ); + expect(form.state().value.prop1).toEqual("value1"); + + form.setProps({ value: { prop1: "value2" } }); + expect(form.state().value.prop1).toEqual("value2"); + }); +}); + +describe("validation warnings", () => { + const fields: FieldDef[] = [ + { + id: "FIELD1", + type: "text", + name: "field1", + validWhen: { + matchesRegEx: { + pattern: "^[\\d]+$", + message: "Numbers only" + } + } + } + ]; + + const onButtonClick = jest.fn(); + const formValue = { + field1: "test" + }; + + describe("shown immediately", () => { + const form = mount( +
+ + + + ); + + test("form state is invalid", () => { + expect(form.state().isValid).toBe(false); + }); + + test("field should be invalid", () => { + expect(form.state().fields[0].isValid).toBe(false); + }); + + test("warning is shown when field is invalid", () => { + expect(form.find("span.errors").length).toBe(1); + }); + + test("warning is displayed as configured", () => { + expect(form.find("span.errors").text()).toBe("Numbers only"); + }); + }); + + describe("shown when touched", () => { + const form = mount( +
+ + + + ); + + test("form state is invalid", () => { + expect(form.state().isValid).toBe(false); + }); + + test("field has not been touched", () => { + expect(form.state().fields[0].touched).toBe(false); + }); + + test("field should be valid (as it's not been touched)", () => { + expect(form.state().fields[0].isValid).toBe(true); + }); + + test("button should still be disabled", () => { + chai.expect(form.find("button")).to.be.disabled(); + }); + + test("field should be discretely invalid (as it's not been touched)", () => { + expect(form.state().fields[0].isDiscretelyInvalid).toBe(true); + }); + + test("warning is not shown as field has not been touched", () => { + expect(form.find("span.errors").length).toBe(0); + }); + + test("field should NOT be touched after focus", () => { + form.find("input").prop("onFocus")(); + expect(form.state().fields[0].touched).toBe(false); + }); + + test("field should be touched after blur", () => { + form.find("input").prop("onBlur")(); + expect(form.state().fields[0].touched).toBe(true); + }); + + test("field should be invalid", () => { + expect(form.state().fields[0].isValid).toBe(false); + }); + + test("field has correct error message", () => { + expect(form.state().fields[0].errorMessages).toBe("Numbers only"); + }); + + test("warning now shown as field has been touched", () => { + form.update(); + expect(form.find("span.errors").length).toBe(1); + }); + + test("warning is displayed as configured", () => { + expect(form.find("span.errors").text()).toBe("Numbers only"); + }); + }); +}); + +describe("changing form value prop", () => { + const fields: FieldDef[] = [ + { + id: "FIELD1", + type: "text", + name: "field1", + validWhen: { + matchesRegEx: { + pattern: "^[\\d]+$", + message: "Numbers only" + } + } + } + ]; + + const onButtonClick = jest.fn(); + const formValue1 = { + field1: "value1" + }; + const formValue2 = { + field1: "value2" + }; + + const form = mount( +
+ + + ); + + test("field has not been touched", () => { + expect(form.state().fields[0].touched).toBe(false); + }); + + test("field should NOT be touched after focus", () => { + form.find("input").prop("onFocus")(); + form.update(); + expect(form.state().fields[0].touched).toBe(false); + }); + + test("field should be touched after blur", () => { + form.find("input").prop("onBlur")(); + form.update(); + expect(form.state().fields[0].touched).toBe(true); + }); + + test("field should not be touched after form value update", () => { + form.setProps({ value: formValue2 }); + form.update(); + expect(form.state().fields[0].touched).toBe(false); + }); +}); + +describe("Form and FormFragment behave the same with defaultFields and value", () => { + // This example is taken from issue: https://github.com/draperd/react-forms-processor/issues/35 + const fruit: FieldDef[] = [ + { + id: "PICKMORETHANONE", + name: "fruit", + label: "Pick some fruit", + placeholder: "Available fruits...", + type: "multiselect", + defaultValue: "apple,banana", + valueDelimiter: ",", + options: [ + { + items: ["apple", "banana", "kiwi", "melon", "grapefruit", "plum"] + } + ] + } + ]; + + test("Form field has correct value from defaultFields", () => { + const form = mount(
); + const fieldValue = form.find("select").prop("value"); + expect(fieldValue).toEqual(["apple", "banana"]); + }); + + test("FormFragment field has correct value from defaultFields", () => { + const form = mount( + + + + ); + const fieldValue = form.find("select").prop("value"); + expect(fieldValue).toEqual(["apple", "banana"]); + }); + + test("Form field has correct value from value", () => { + const form = mount( +
+ ); + const fieldValue = form.find("select").prop("value"); + expect(fieldValue).toEqual(["melon"]); + }); + + test("FormFragment field has correct value from value", () => { + const form = mount( + + + + ); + const fieldValue = form.find("select").prop("value"); + expect(fieldValue).toEqual(["melon"]); + }); +}); describe("Form with initial value updates value on field change", () => { const fields: FieldDef[] = [ From 5b579321e9ef8833ca4b5f4a2545fa1c7a66a0e0 Mon Sep 17 00:00:00 2001 From: Dave Draper Date: Mon, 18 Feb 2019 11:55:07 +0000 Subject: [PATCH 3/5] WIP --- packages/core/src/components/Form.js | 16 +++++++++++++++- packages/core/src/components/FormFragment.js | 16 +++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/core/src/components/Form.js b/packages/core/src/components/Form.js index 9aec695..9a68612 100644 --- a/packages/core/src/components/Form.js +++ b/packages/core/src/components/Form.js @@ -118,11 +118,18 @@ export default class Form extends Component< // Merge value from fields (if set) into form value... // This is done to ensure the form value has the latest value defined... + // console.log("Next props", nextProps); + + // TODO: Don't use the field value if the value prop has changed... + // if (valueFromProps) { defaultFields.forEach(field => { if (field.value) { value[field.name] = field.value; } }); + // } + + console.log("Form value", value); let fields; if (defaultFieldsFromProps && defaultFieldsChange) { @@ -149,7 +156,8 @@ export default class Form extends Component< ...nextState, defaultFields: defaultFieldsFromProps, disabled, - showValidationBeforeTouched + showValidationBeforeTouched, + value }; } else { return null; @@ -312,7 +320,13 @@ export default class Form extends Component< } }); + console.log( + "Value when rendering form", + this.props.value, + this.state.value + ); const context = this.createFormContext(); + console.log("Context value", context.value); return ( {defaultFields && } diff --git a/packages/core/src/components/FormFragment.js b/packages/core/src/components/FormFragment.js index b45abeb..02aa756 100644 --- a/packages/core/src/components/FormFragment.js +++ b/packages/core/src/components/FormFragment.js @@ -38,6 +38,7 @@ const setFieldValue = ( ): void => { const { name, omitWhenValueIs, value } = defaultDefinition; const formValueForName = formValue[name]; + // console.log("Values", formValue, value); if ( omitWhenValueIs && omitWhenValueIs.find(targetValue => targetValue === formValueForName) === -1 @@ -75,14 +76,19 @@ class FormFragment extends Component< > { constructor(props: FormFragmentComponentWithContextProps) { super(props); - const { defaultFields = [], registerField, fields = [] } = props; - defaultFields.forEach(field => - registerFieldIfNew(field, fields, registerField) - ); + const { defaultFields = [], registerField, fields = [], value } = props; + console.log("Value of fragment when created", value); + defaultFields.forEach(field => { + field.defaultValue = + value[field.name] !== undefined + ? value[field.name] + : field.defaultValue; + registerFieldIfNew(field, fields, registerField); + }); } render() { - const { defaultFields = [] } = this.props; + const { defaultFields = [], value } = this.props; const renderedFields = defaultFields.map(field => renderFieldIfVisible(field, this.props) ); From 8e0799c4dbf1a54aada645c70c296fa9d0c4bb19 Mon Sep 17 00:00:00 2001 From: Dave Draper Date: Sat, 2 Mar 2019 09:58:43 +0000 Subject: [PATCH 4/5] Added defaultValue prop to Form component --- packages/core/package.json | 120 +++++++++---------- packages/core/src/components/Form.js | 62 +++++----- packages/core/src/components/Form.test.js | 16 +-- packages/core/src/components/FormFragment.js | 2 - packages/core/src/types.js | 1 + packages/core/src/types/components.js | 1 + 6 files changed, 97 insertions(+), 105 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 55b13b6..8786536 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,62 +1,62 @@ { - "name": "react-forms-processor", - "version": "0.0.35", - "description": "A forms processor for statically declaring forms with dynamic behaviour", - "main": "dist/index.js", - "scripts": { - "test": "jest", - "glow": "glow", - "glow:watch": "glow --watch", - "start": "webpack-dev-server --mode development", - "transpile": "babel src -d dist --copy-files", - "prepublishOnly": "npm run transpile", - "build": "webpack --mode production", - "dev:watch": "webpack --mode development --watch", - "postbuild": "yarn build:flow", - "build:flow": "../../node_modules/flow-copy-source/bin/flow-copy-source.js --verbose --ignore '**/*{story,test}.js' src dist" - }, - "license": "MIT", - "peerDependencies": { - "react": "^16.4", - "react-dom": "^16.4" - }, - "devDependencies": { - "babel-cli": "^6.26.0", - "babel-core": "^6.26.3", - "babel-jest": "^22.4.3", - "babel-loader": "^7.1.4", - "babel-plugin-transform-class-properties": "^6.24.1", - "babel-plugin-transform-object-rest-spread": "^6.26.0", - "babel-preset-env": "^1.7.0", - "babel-preset-jest": "^22.4.3", - "babel-preset-react": "^6.24.1", - "chai": "^4.2.0", - "chai-enzyme": "^1.0.0-beta.1", - "css-loader": "^0.28.11", - "enzyme": "^3.7.0", - "enzyme-adapter-react-16.3": "^1.3.0", - "flow-bin": "0.79.1", - "flow-types": "^1.0.0", - "gh-pages": "^1.1.0", - "glow": "^1.1.1", - "html-webpack-plugin": "^3.2.0", - "jest": "^22.4.3", - "react": "^16.4", - "react-dom": "^16.4", - "react-test-renderer": "^16.4", - "regenerator-runtime": "^0.11.1", - "style-loader": "^0.21.0", - "webpack": "^4.8.1", - "webpack-cli": "^2.1.3", - "webpack-dev-server": "^3.1.4" - }, - "dependencies": { - "flow-copy-source": "^2.0.2", - "lodash": "^4.17.10" - }, - "jest": { - "verbose": true, - "bail": false, - "rootDir": "src" - } + "name": "react-forms-processor", + "version": "0.0.35", + "description": "A forms processor for statically declaring forms with dynamic behaviour", + "main": "dist/index.js", + "scripts": { + "test": "jest", + "glow": "glow", + "glow:watch": "glow --watch", + "start": "webpack-dev-server --mode development", + "transpile": "babel src -d dist --copy-files", + "prepublishOnly": "npm run transpile", + "build": "webpack --mode production", + "dev:watch": "webpack --mode development --watch", + "postbuild": "yarn build:flow", + "build:flow": "../../node_modules/flow-copy-source/bin/flow-copy-source.js --verbose --ignore '**/*{story,test}.js' src dist" + }, + "license": "MIT", + "peerDependencies": { + "react": "^16.4", + "react-dom": "^16.4" + }, + "devDependencies": { + "babel-cli": "^6.26.0", + "babel-core": "^6.26.3", + "babel-jest": "^22.4.3", + "babel-loader": "^7.1.4", + "babel-plugin-transform-class-properties": "^6.24.1", + "babel-plugin-transform-object-rest-spread": "^6.26.0", + "babel-preset-env": "^1.7.0", + "babel-preset-jest": "^22.4.3", + "babel-preset-react": "^6.24.1", + "chai": "^4.2.0", + "chai-enzyme": "^1.0.0-beta.1", + "css-loader": "^0.28.11", + "enzyme": "^3.7.0", + "enzyme-adapter-react-16.3": "^1.3.0", + "flow-bin": "0.79.1", + "flow-types": "^1.0.0", + "gh-pages": "^1.1.0", + "glow": "^1.1.1", + "html-webpack-plugin": "^3.2.0", + "jest": "^22.4.3", + "react": "^16.4", + "react-dom": "^16.4", + "react-test-renderer": "^16.4", + "regenerator-runtime": "^0.11.1", + "style-loader": "^0.21.0", + "webpack": "^4.8.1", + "webpack-cli": "^2.1.3", + "webpack-dev-server": "^3.1.4" + }, + "dependencies": { + "flow-copy-source": "^2.0.2", + "lodash": "^4.17.10" + }, + "jest": { + "verbose": false, + "bail": false, + "rootDir": "src" + } } diff --git a/packages/core/src/components/Form.js b/packages/core/src/components/Form.js index 9a68612..00750e5 100644 --- a/packages/core/src/components/Form.js +++ b/packages/core/src/components/Form.js @@ -33,11 +33,6 @@ const valueHasChanged = ( prevState: FormComponentState ) => nextProps.value && nextProps.value !== prevState.value; -const defaultValueHasChanged = ( - nextProps: FormComponentProps, - prevState: FormComponentState -) => nextProps.value && nextProps.value !== prevState.defaultValue; - const formDisabledStateHasChanged = ( nextProps: FormComponentProps, prevState: FormComponentState @@ -59,8 +54,8 @@ export default class Form extends Component< super(props); this.state = { fields: [], - defaultValue: props.value || {}, - value: props.value || {}, + defaultValue: props.defaultValue || {}, + value: props.value || props.defaultValue || {}, isValid: false, defaultFields: [], disabled: props.disabled || false, @@ -90,17 +85,22 @@ export default class Form extends Component< prevState: FormComponentState ) { const defaultFieldsChange = defaultFieldsHaveChanged(nextProps, prevState); - const defaultValueChange = defaultValueHasChanged(nextProps, prevState); + const valueChange = valueHasChanged(nextProps, prevState); if ( defaultFieldsChange || - valueHasChanged(nextProps, prevState) || + valueChange || formDisabledStateHasChanged(nextProps, prevState) || formTouchedBehaviourHasChanged(nextProps, prevState) ) { - const { fields: fieldsFromState, value: valueFromState } = prevState; + const { + fields: fieldsFromState, + defaultValue: defaultValueFromState, + value: valueFromState + } = prevState; let { defaultFields: defaultFieldsFromProps, + defaultValue: defaultValueFromProps, value: valueFromProps, disabled = false } = nextProps; @@ -111,25 +111,22 @@ export default class Form extends Component< showValidationBeforeTouched = false } = nextProps; - // If a new value has been passed to the Form as a prop then it should take precedence over the last calculated state - const value = valueFromProps || valueFromState || {}; + const value = { + ...defaultValueFromProps, + ...valueFromState, + ...valueFromProps + }; const defaultFields = defaultFieldsFromProps || fieldsFromState; - // Merge value from fields (if set) into form value... - // This is done to ensure the form value has the latest value defined... - // console.log("Next props", nextProps); - // TODO: Don't use the field value if the value prop has changed... - // if (valueFromProps) { - defaultFields.forEach(field => { - if (field.value) { - value[field.name] = field.value; - } - }); - // } - - console.log("Form value", value); + if (!valueChange) { + defaultFields.forEach(field => { + if (field.value) { + value[field.name] = field.value; + } + }); + } let fields; if (defaultFieldsFromProps && defaultFieldsChange) { @@ -141,7 +138,7 @@ export default class Form extends Component< // We should reset the touched state of all the fields if the value passed as a prop to the form // changes... - const resetTouchedState = defaultValueChange; + const resetTouchedState = valueChange; const nextState = getNextStateFromFields({ fields, @@ -155,13 +152,13 @@ export default class Form extends Component< return { ...nextState, defaultFields: defaultFieldsFromProps, + defaultValue: defaultValueFromProps || defaultValueFromState, disabled, showValidationBeforeTouched, value }; - } else { - return null; } + return null; } onFieldChange(id: string, value: Value) { @@ -272,7 +269,7 @@ export default class Form extends Component< } createFormContext() { - const { fields, value, isValid } = this.state; + const { fields, defaultValue, value, isValid } = this.state; const { renderer = defaultRenderer, optionsHandler, @@ -289,6 +286,7 @@ export default class Form extends Component< const context: FormContextData = { fields, isValid, + defaultValue, value, registerField: this.registerField.bind(this), renderer, @@ -320,13 +318,7 @@ export default class Form extends Component< } }); - console.log( - "Value when rendering form", - this.props.value, - this.state.value - ); const context = this.createFormContext(); - console.log("Context value", context.value); return ( {defaultFields && } diff --git a/packages/core/src/components/Form.test.js b/packages/core/src/components/Form.test.js index 4e47381..c376961 100644 --- a/packages/core/src/components/Form.test.js +++ b/packages/core/src/components/Form.test.js @@ -31,7 +31,7 @@ describe("Context", () => {
{context => { @@ -58,7 +58,7 @@ describe("Context", () => { const propValue = { prop1: "value1" }; let value; const form = mount( - + ); @@ -91,7 +91,7 @@ describe("validation warnings", () => { describe("shown immediately", () => { const form = mount( -
+ @@ -116,7 +116,7 @@ describe("validation warnings", () => { describe("shown when touched", () => { const form = mount( -
+ @@ -199,7 +199,7 @@ describe("changing form value prop", () => { }; const form = mount( -
+ ); @@ -264,7 +264,7 @@ describe("Form and FormFragment behave the same with defaultFields and value", ( test("Form field has correct value from value", () => { const form = mount( -
+ ); const fieldValue = form.find("select").prop("value"); expect(fieldValue).toEqual(["melon"]); @@ -272,7 +272,7 @@ describe("Form and FormFragment behave the same with defaultFields and value", ( test("FormFragment field has correct value from value", () => { const form = mount( - + ); @@ -294,7 +294,7 @@ describe("Form with initial value updates value on field change", () => { }; const form = mount( -
+ ); diff --git a/packages/core/src/components/FormFragment.js b/packages/core/src/components/FormFragment.js index 02aa756..460bc46 100644 --- a/packages/core/src/components/FormFragment.js +++ b/packages/core/src/components/FormFragment.js @@ -38,7 +38,6 @@ const setFieldValue = ( ): void => { const { name, omitWhenValueIs, value } = defaultDefinition; const formValueForName = formValue[name]; - // console.log("Values", formValue, value); if ( omitWhenValueIs && omitWhenValueIs.find(targetValue => targetValue === formValueForName) === -1 @@ -77,7 +76,6 @@ class FormFragment extends Component< constructor(props: FormFragmentComponentWithContextProps) { super(props); const { defaultFields = [], registerField, fields = [], value } = props; - console.log("Value of fragment when created", value); defaultFields.forEach(field => { field.defaultValue = value[field.name] !== undefined diff --git a/packages/core/src/types.js b/packages/core/src/types.js index c06366d..876c148 100644 --- a/packages/core/src/types.js +++ b/packages/core/src/types.js @@ -342,6 +342,7 @@ export type OmitFieldValue = FieldDef => boolean; export type FormContextData = { fields: FieldDef[], + defaulValue: FormValue, value: FormValue, isValid: boolean, options: { diff --git a/packages/core/src/types/components.js b/packages/core/src/types/components.js index dc37189..82aa1e5 100644 --- a/packages/core/src/types/components.js +++ b/packages/core/src/types/components.js @@ -14,6 +14,7 @@ import type { export type FormComponentProps = { defaultFields?: FieldDef[], + defaultValue?: FormValue, value?: FormValue, onChange?: OnFormChange, onFieldFocus?: OnFieldFocus, From 6373f34f720793eb8eed9f44ed8aa3bb7ddaa6e4 Mon Sep 17 00:00:00 2001 From: Dave Draper Date: Sat, 2 Mar 2019 10:00:39 +0000 Subject: [PATCH 5/5] Fixed flow error --- packages/core/src/types.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/types.js b/packages/core/src/types.js index 876c148..16b4a4c 100644 --- a/packages/core/src/types.js +++ b/packages/core/src/types.js @@ -342,7 +342,7 @@ export type OmitFieldValue = FieldDef => boolean; export type FormContextData = { fields: FieldDef[], - defaulValue: FormValue, + defaultValue: FormValue, value: FormValue, isValid: boolean, options: {