diff --git a/spec/pivotal-ui-react/forms/form-col_spec.js b/spec/pivotal-ui-react/forms/form-col_spec.js
deleted file mode 100644
index 375f83eea..000000000
--- a/spec/pivotal-ui-react/forms/form-col_spec.js
+++ /dev/null
@@ -1,91 +0,0 @@
-import '../spec_helper';
-import {FormCol} from '../../../src/react/forms';
-import crypto from 'crypto';
-import {FlexCol} from '../../../src/react/flex-grids';
-
-describe('FormCol', () => {
- let onChange, state, setState;
-
- beforeEach(() => {
- spyOn(React, 'cloneElement').and.callThrough();
- spyOn(crypto, 'randomBytes').and.returnValue('some-unique-string');
- onChange = jasmine.createSpy('onChange');
- setState = jasmine.createSpy('setState');
- state = {key: 'value'};
- });
-
- describe('when fixed', () => {
- beforeEach(() => ReactDOM.render(, root));
- it('renders a col-fixed class', () => expect('.col').toHaveClass('col-fixed'));
- });
-
- describe('with id', () => {
- beforeEach(() => ReactDOM.render(, root));
- it('renders with the id', () => expect('.col').toHaveAttr('id', 'some-id'));
- });
-
- describe('when hidden', () => {
- beforeEach(() => ReactDOM.render(, root));
- it('renders hidden', () => expect('.col').toHaveAttr('hidden', ''));
- });
-
- describe('with children', () => {
- beforeEach(() => {
- spyOnRender(FlexCol).and.callThrough();
-
- ReactDOM.render(
- hello
- , root);
- });
-
- it('renders a FlexCol with the props', () => {
- expect(FlexCol).toHaveBeenRenderedWithProps({
- id: 'some-id',
- className: 'some-class form-col',
- hidden: undefined,
- children: jasmine.any(Object)
- });
- });
-
- it('renders children', () => {
- expect('.col > div').toHaveText('hello');
- });
- });
-
- describe('with a function child', () => {
- let child, state, setState, canSubmit, onSubmit, canReset, reset;
-
- beforeEach(() => {
- spyOnRender(FlexCol).and.callThrough();
- child = jasmine.createSpy('child').and.returnValue(child return value);
- state = {submitting: true};
- setState = jasmine.createSpy('setState');
- canSubmit = jasmine.createSpy('canSubmit');
- onSubmit = jasmine.createSpy('onSubmit');
- canReset = jasmine.createSpy('canReset');
- reset = jasmine.createSpy('reset');
- ReactDOM.render({child}, root);
- });
-
- it('renders a FlexCol with the props', () => {
- expect(FlexCol).toHaveBeenRenderedWithProps({
- id: 'some-id',
- className: 'some-class form-col',
- hidden: undefined,
- children: jasmine.any(Object)
- });
- });
-
- it('calls the child', () => {
- expect(child).toHaveBeenCalledWith({
- canSubmit, canReset, reset, onSubmit, submitting: state.submitting, setState, state
- });
- });
-
- it('renders the child return value', () => {
- expect('.col > span.child').toHaveText('child return value');
- });
- });
-});
\ No newline at end of file
diff --git a/spec/pivotal-ui-react/forms/form-row_spec.js b/spec/pivotal-ui-react/forms/form-row_spec.js
deleted file mode 100644
index cff232e71..000000000
--- a/spec/pivotal-ui-react/forms/form-row_spec.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import '../spec_helper';
-import {FormRow, FormCol} from '../../../src/react/forms';
-import {Grid} from '../../../src/react/flex-grids';
-
-describe('FormRow', () => {
- let subject, field1, field2;
-
- beforeEach(() => {
- spyOnRender(FormCol).and.callThrough();
-
- field1 = () =>
hello
;
- field2 = () => test;
-
- subject = ReactDOM.render(
-
-
- , root);
- });
-
- it('renders a grid containing the children', () => {
- expect('#root > .grid .col').toHaveLength(2);
- expect('#root > .grid .col:eq(0)').toHaveText('hello');
- expect('#root > .grid .col:eq(1)').toHaveText('test');
- });
-
- describe('when given a wrapper', () => {
- beforeEach(() => {
- subject::setProps({wrapper: () => });
- });
-
- it('wraps its children in the wrapper', () => {
- expect('#root > .grid .col').not.toExist();
- expect('#root > .wrapper .grid .col').toHaveLength(2);
- expect('#root > .wrapper .grid .col:eq(0)').toHaveText('hello');
- expect('#root > .wrapper .grid .col:eq(1)').toHaveText('test');
- });
- });
-
- describe('when given a class name and an ID', () => {
- beforeEach(() => {
- spyOnRender(Grid).and.callThrough();
- subject::setProps({className: 'my-form-row', id: 'my-form-row-id'});
- });
-
- it('puts the class name and the ID on the inner form row grid', () => {
- expect(Grid).toHaveBeenRenderedWithProps({
- className: 'my-form-row form-row',
- id: 'my-form-row-id',
- gutter: true,
- children: jasmine.any(Object)
- });
- });
- });
-});
\ No newline at end of file
diff --git a/spec/pivotal-ui-react/forms/form-unit_spec.js b/spec/pivotal-ui-react/forms/form-unit_spec.js
index 1a7f04a8f..4d843d027 100644
--- a/spec/pivotal-ui-react/forms/form-unit_spec.js
+++ b/spec/pivotal-ui-react/forms/form-unit_spec.js
@@ -9,20 +9,20 @@ describe('FormUnit', () => {
spyOnRender(TooltipTrigger).and.callThrough();
subject = ReactDOM.render(hello)
- }} />, root);
+ children: (hello
)
+ }}/>, root);
});
it('does not render a label row when no label is provided', () => {
expect('.form-unit .label-row').not.toExist();
});
- describe('when no field is provided', () => {
+ describe('when no children are provided', () => {
beforeEach(() => {
subject = ReactDOM.render(, root);
+ }}/>, root);
});
it('does not render a field row', () => {
@@ -161,18 +161,18 @@ describe('FormUnit', () => {
});
describe('when the postLabel is a function', () => {
- let postLabel, setState, state;
+ let postLabel, state, setValues;
beforeEach(() => {
postLabel = jasmine.createSpy('postLabel').and.returnValue(returned);
- setState = jasmine.createSpy('setState');
+ setValues = jasmine.createSpy('setValues');
state = {key: 'value'};
- subject::setProps({postLabel, setState, state});
+ subject::setProps({postLabel, state, setValues});
});
it('calls the postLabel function', () => {
- expect(postLabel).toHaveBeenCalledWith({setState, state});
+ expect(postLabel).toHaveBeenCalledWith({state, setValues});
});
it('renders the returned node', () => {
@@ -373,9 +373,9 @@ describe('FormUnit', () => {
});
});
- describe('when there is no label, field, or help block', () => {
+ describe('when there is no label, children, or help block', () => {
beforeEach(() => {
- subject::setProps({label: null, field: null, help: null});
+ subject::setProps({label: null, children: null, help: null});
});
it('renders nothing', () => {
diff --git a/spec/pivotal-ui-react/forms/form_spec.js b/spec/pivotal-ui-react/forms/form_spec.js
index 6336b4cd4..f907aab3c 100644
--- a/spec/pivotal-ui-react/forms/form_spec.js
+++ b/spec/pivotal-ui-react/forms/form_spec.js
@@ -48,9 +48,12 @@ describe('Form', () => {
});
describe('with one required field', () => {
+ let fields;
+
beforeEach(() => {
+ fields = {name: {initialValue: 'some-name'}};
subject = ReactDOM.render(
- , root);
+ });
+
+ it('stores empty string in the state', () => {
+ expect(subject.state.initial.name).toBe('');
+ expect(subject.state.current.name).toBe('');
+ });
+ });
+
+ describe('with one required field where initialValue is undefined', () => {
+ let fields;
+
+ beforeEach(() => {
+ fields = {name: {initialValue: undefined}};
+ subject = ReactDOM.render(
+ , root);
+ });
+
+ it('stores empty string in the state', () => {
+ expect(subject.state.initial.name).toBe('');
+ expect(subject.state.current.name).toBe('');
+ });
+ });
+
+ describe('with one required field where initialValue is null', () => {
+ let fields;
+
+ beforeEach(() => {
+ fields = {name: {initialValue: null}};
+ subject = ReactDOM.render(
+ , root);
+ });
+
+ it('stores empty string in the state', () => {
+ expect(subject.state.initial.name).toBe('');
+ expect(subject.state.current.name).toBe('');
+ });
+ });
+
+ describe('with one required field where initialValue is false', () => {
+ let fields;
+
+ beforeEach(() => {
+ fields = {name: {initialValue: false}};
+ subject = ReactDOM.render(
+ , root);
+ });
+
+ it('stores empty string in the state', () => {
+ expect(subject.state.initial.name).toBe(false);
+ expect(subject.state.current.name).toBe(false);
+ });
+ });
+
+ describe('with one required field where initialValue is zero', () => {
+ let fields;
+
+ beforeEach(() => {
+ fields = {name: {initialValue: 0}};
+ subject = ReactDOM.render(
+ , root);
+ });
+
+ it('stores empty string in the state', () => {
+ expect(subject.state.initial.name).toBe(0);
+ expect(subject.state.current.name).toBe(0);
+ });
+ });
+
describe('with two required fields', () => {
beforeEach(() => {
subject = ReactDOM.render(
@@ -469,6 +589,54 @@ describe('Form', () => {
});
});
+ describe('with two required fields, one required based on optional callback', () => {
+ let optional;
+
+ beforeEach(() => {
+ optional = jasmine.createSpy('optional');
+ subject = ReactDOM.render(
+ , root);
+ });
+
+ it('renders inputs without values', () => {
+ expect('fieldset > .grid:eq(0) > .col:eq(0) input').toHaveValue('');
+ expect('fieldset > .grid:eq(0) > .col:eq(1) input').toHaveValue('');
+ });
+
+ it('renders disabled buttons in a col-fixed col', () => {
+ expect('.grid:eq(0) .col:eq(2)').toHaveClass('col-fixed');
+ expect('.grid:eq(0) .col:eq(2) .save').toHaveAttr('type', 'submit');
+ expect('.grid:eq(0) .col:eq(2) .save').toHaveText('Save');
+ expect('.grid:eq(0) .col:eq(2) .save').toBeDisabled();
+ expect('.grid:eq(0) .col:eq(2) .cancel').toHaveText('Cancel');
+ expect('.grid:eq(0) .col:eq(2) .cancel').toBeDisabled();
+ });
+
+ describe('when setting the name', () => {
+ beforeEach(() => {
+ $('fieldset > .grid:eq(0) > .col:eq(0) input').val('some-other-name').simulate('change');
+ });
+
+ it('allows the name to change', () => {
+ expect('fieldset > .grid:eq(0) > .col:eq(0) input').toHaveValue('some-other-name');
+ });
+
+ it('renders buttons ', () => {
+ expect('.grid:eq(0) .col:eq(2)').toHaveClass('col-fixed');
+ expect('.grid:eq(0) .col:eq(2) .save').toBeDisabled();
+ expect('.grid:eq(0) .col:eq(2) .cancel').not.toBeDisabled();
+ });
+ });
+ });
+
describe('with one optional field', () => {
beforeEach(() => {
subject = ReactDOM.render(
@@ -816,7 +984,7 @@ describe('Form', () => {
describe('when passed extra props', () => {
beforeEach(() => {
ReactDOM.render(
- , root);
+ , root);
});
it('passes them to the form tag', () => {
@@ -828,6 +996,32 @@ describe('Form', () => {
describe('when rendering a Page component', () => {
beforeEach(() => {
+ class Page extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {renderCol: false};
+ }
+
+ render() {
+ const {renderCol} = this.state;
+
+ return (
+
+
+ this.setState({renderCol: !renderCol})}/>
+
+ );
+ }
+ }
+
subject = ReactDOM.render(, root);
$('fieldset > .grid:eq(0) > .col:eq(0) input').val('some-name').simulate('change');
});
@@ -843,8 +1037,7 @@ describe('Form', () => {
submitting: false,
errors: {},
initial: {name: '', other: ''},
- current: {name: 'some-name', other: ''},
- requiredFields: ['name', 'other']
+ current: {name: 'some-name', other: ''}
});
});
@@ -865,14 +1058,13 @@ describe('Form', () => {
submitting: false,
errors: {},
initial: {name: '', password: '', other: ''},
- current: {name: 'some-name', password: '', other: ''},
- requiredFields: ['name', 'password', 'other']
+ current: {name: 'some-name', password: '', other: ''}
});
});
});
});
- describe('when passing the onChange', () => {
+ describe('when passing values to built-in onChange', () => {
let subject;
describe('when passed an event', () => {
@@ -889,15 +1081,15 @@ describe('Form', () => {
});
});
- describe('when passed a value', () => {
+ describe('when not passed an event', () => {
beforeEach(() => {
- const Component = ({onChange}) => onChange('some-title')}}/>;
+ const Component = ({onChange}) => ;
Component.propTypes = {onChange: PropTypes.func};
subject = ReactDOM.render(
}}}}>
{({fields: {title}}) => {title}}
, root);
- $('.form input').val('').simulate('change');
+ $('.form input').val('some-title').simulate('change');
});
it('uses the value', () => {
@@ -905,4 +1097,76 @@ describe('Form', () => {
});
});
});
+
+ describe('when a field has a custom onChange', () => {
+ let persist;
+
+ beforeEach(() => {
+ persist = jasmine.createSpy('persist');
+ subject = ReactDOM.render(
+ , root);
+ $('.form input').val('some-title').simulate('change', {target: {value: 'some-title'}, persist});
+ });
+
+ it('persists the event', () => {
+ expect(persist).toHaveBeenCalledWith();
+ });
+
+ it('updates the current state', () => {
+ expect(subject.state.current).toEqual({title: 'some-title', name: 'Jane'});
+ });
+ });
+
+ describe('when a checkbox field has a custom onChange', () => {
+ beforeEach(() => {
+ subject = ReactDOM.render(
+ , root);
+ $('.form input').click();
+ });
+
+ it('updates the current state', () => {
+ expect(subject.state.current).toEqual({title: true, name: 'Jane'});
+ });
+ });
+
+ describe('when updating current values programmatically', () => {
+ beforeEach(() => {
+ subject = ReactDOM.render(
+ , root);
+
+ subject.setValues({name: 'new-name'});
+ });
+
+ it('changes the form state without updating un-passed values', () => {
+ expect(subject.state.current).toEqual({name: 'new-name', password: 'some-password'});
+ });
+ });
});
\ No newline at end of file
diff --git a/spec/pivotal-ui-react/forms/grid-form_spec.js b/spec/pivotal-ui-react/forms/grid-form_spec.js
deleted file mode 100644
index 8879b1211..000000000
--- a/spec/pivotal-ui-react/forms/grid-form_spec.js
+++ /dev/null
@@ -1,1001 +0,0 @@
-import '../spec_helper';
-import {GridForm, FormRow, FormCol} from '../../../src/react/forms';
-import {Input} from '../../../src/react/inputs';
-import {DefaultButton} from '../../../src/react/buttons';
-import {Checkbox} from '../../../src/react/checkbox';
-import React from 'react';
-import PropTypes from 'prop-types';
-
-class Page extends React.Component {
- constructor(props) {
- super(props);
- this.state = {renderCol: false};
- }
-
- render() {
- const {renderCol} = this.state;
-
- return (
-
- this.gridForm = el}>
-
-
-
-
- {renderCol &&
-
- }
-
-
-
-
-
- this.setState({renderCol: !renderCol})}/>
-
- );
- }
-}
-
-describe('GridForm', () => {
- let Buttons, subject, afterSubmit;
-
- beforeEach(() => {
- afterSubmit = jasmine.createSpy('afterSubmit');
- Buttons = jasmine.createSpy('Buttons');
- Buttons.and.callFake(({canSubmit, canReset, reset}) => (
-
- Save
- Cancel
-
- ));
- });
-
- describe('with one required field', () => {
- beforeEach(() => {
- subject = ReactDOM.render(
-
-
-
-
-
-
-
- , root);
- });
-
- it('uses the form classname', () => {
- expect('form').toHaveClass('some-form');
- });
-
- it('does not disable the top-level fieldset', () => {
- expect('form > fieldset').not.toBeDisabled();
- });
-
- it('renders an input with a default value', () => {
- expect('.form-row:eq(0) .form-col:eq(0) input').toHaveAttr('value', 'some-name');
- });
-
- it('renders Buttons with submitting=false', () => {
- expect(Buttons).toHaveBeenCalledWith({
- canSubmit: subject.form.canSubmit,
- canReset: subject.form.canReset,
- reset: subject.form.reset,
- onSubmit: subject.form.onSubmit,
- submitting: false,
- setState: subject.form.setState,
- state: subject.form.state
- });
- });
-
- it('renders disabled buttons in a col-fixed col', () => {
- expect('.form-row:eq(0) .form-col:eq(1)').toHaveClass('col-fixed');
- expect('.form-row:eq(0) .form-col:eq(1) .save').toHaveAttr('type', 'submit');
- expect('.form-row:eq(0) .form-col:eq(1) .save').toHaveText('Save');
- expect('.form-row:eq(0) .form-col:eq(1) .save').toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(1) .cancel').toHaveText('Cancel');
- expect('.form-row:eq(0) .form-col:eq(1) .cancel').toBeDisabled();
- });
-
- describe('when deleting the name', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(0) input').val('').simulate('change');
- });
-
- it('allows the name to change', () => {
- expect('.form-row:eq(0) .form-col:eq(0) input').toHaveAttr('value', '');
- });
-
- it('renders buttons ', () => {
- expect('.form-row:eq(0) .form-col:eq(1)').toHaveClass('col-fixed');
- expect('.form-row:eq(0) .form-col:eq(1) .save').toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(1) .cancel').not.toBeDisabled();
- });
-
- describe('when clicking the cancel button', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(1) .cancel').simulate('click');
- });
-
- it('resets the name', () => {
- expect('.form-row:eq(0) .form-col:eq(0) input').toHaveAttr('value', 'some-name');
- });
- });
- });
-
- describe('when changing the name', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(0) input').val('some-other-name').simulate('change');
- });
-
- it('allows the name to change', () => {
- expect('.form-row:eq(0) .form-col:eq(0) input').toHaveAttr('value', 'some-other-name');
- });
-
- it('renders enabled buttons ', () => {
- expect('.form-row:eq(0) .form-col:eq(1)').toHaveClass('col-fixed');
- expect('.form-row:eq(0) .form-col:eq(1) .save').not.toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(1) .cancel').not.toBeDisabled();
- });
-
- describe('when clicking the cancel button', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(1) .cancel').simulate('click');
- });
-
- it('resets the name', () => {
- expect('.form-row:eq(0) .form-col:eq(0) input').toHaveAttr('value', 'some-name');
- });
- });
-
- describe('when clicking the update button', () => {
- let error, errors, onSubmitError, onSubmit, resolve, reject;
-
- beforeEach(() => {
- Promise.onPossiblyUnhandledRejection(jasmine.createSpy('reject'));
- error = new Error('invalid');
- errors = {name: 'invalid'};
- onSubmitError = jasmine.createSpy('onSubmitError').and.returnValue(errors);
- onSubmit = jasmine.createSpy('onSubmit');
- onSubmit.and.callFake(() => new Promise((res, rej) => {
- resolve = res;
- reject = rej;
- }));
- Buttons.calls.reset();
- subject::setProps({onSubmitError, onSubmit});
- $('.form-row:eq(0) .form-col:eq(1) .save').simulate('submit');
- });
-
- it('calls onSubmit', () => {
- expect(onSubmit).toHaveBeenCalledWith({initial: {name: 'some-name'}, current: {name: 'some-other-name'}});
- });
-
- it('disables the top-level fieldset', () => {
- expect('form > fieldset').toBeDisabled();
- });
-
- it('disables the buttons', () => {
- expect('.form-row:eq(0) .form-col:eq(1) .save').toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(1) .cancel').toBeDisabled();
- });
-
- it('renders Buttons with submitting=true', () => {
- expect(Buttons).toHaveBeenCalledWith({
- canSubmit: subject.form.canSubmit,
- canReset: subject.form.canReset,
- reset: subject.form.reset,
- onSubmit: subject.form.onSubmit,
- submitting: true,
- setState: subject.form.setState,
- state: subject.form.state
- });
- });
-
- describe('when the submit promise resolves', () => {
- beforeEach(() => {
- resolve({result: 'success'});
- MockPromises.tick(1);
- });
-
- it('enables the top-level fieldset', () => {
- expect('form > fieldset').not.toBeDisabled();
- });
-
- it('renders Buttons with submitting=false', () => {
- expect(Buttons).toHaveBeenCalledWith({
- canSubmit: subject.form.canSubmit,
- canReset: subject.form.canReset,
- reset: subject.form.reset,
- onSubmit: subject.form.onSubmit,
- submitting: false,
- setState: subject.form.setState,
- state: subject.form.state
- });
- });
-
- it('makes both buttons disabled', () => {
- expect('.form-row:eq(0) .form-col:eq(1) .save').toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(1) .cancel').toBeDisabled();
- });
-
- it('resets the initial state', () => {
- expect(subject.form.state.initial).toEqual({name: 'some-other-name'});
- });
-
- it('retains the current state', () => {
- expect(subject.form.state.current).toEqual({name: 'some-other-name'});
- });
-
- it('calls the afterSubmit callback', () => {
- MockPromises.tick();
- expect(afterSubmit).toHaveBeenCalledWith({
- state: subject.form.state,
- setState: subject.form.setState,
- response: {result: 'success'},
- reset: subject.form.reset
- });
- });
- });
-
- describe('when the submit promise rejects', () => {
- beforeEach(() => {
- reject(error);
- MockPromises.tick(2);
- });
-
- it('enables the top-level fieldset', () => {
- expect('form > fieldset').not.toBeDisabled();
- });
-
- it('renders Buttons with submitting=false', () => {
- expect(Buttons).toHaveBeenCalledWith({
- canSubmit: subject.form.canSubmit,
- canReset: subject.form.canReset,
- reset: subject.form.reset,
- onSubmit: subject.form.onSubmit,
- submitting: false,
- setState: subject.form.setState,
- state: subject.form.state
- });
- });
-
- it('re-enables both buttons disabled', () => {
- expect('.form-row:eq(0) .form-col:eq(1) .save').not.toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(1) .cancel').not.toBeDisabled();
- });
-
- it('retains the initial state', () => {
- expect(subject.form.state.initial).toEqual({name: 'some-name'});
- });
-
- it('retains the current state', () => {
- expect(subject.form.state.current).toEqual({name: 'some-other-name'});
- });
-
- it('calls the onSubmitError', () => {
- expect(onSubmitError).toHaveBeenCalledWith(error);
- });
-
- it('sets the errors on the state', () => {
- expect(subject.form.state.errors).toBe(errors);
- });
-
- describe('when clicking the cancel button', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(1) .cancel').simulate('click');
- });
-
- it('resets the name', () => {
- expect('.form-row:eq(0) .form-col:eq(0) input').toHaveAttr('value', 'some-name');
- });
-
- it('clears the errors', () => {
- expect(subject.form.state.errors).toEqual({});
- });
- });
-
- describe('when clicking the save button and the submit action resolves', () => {
- beforeEach(() => {
- onSubmit.and.returnValue(Promise.resolve());
- $('.form-row:eq(0) .form-col:eq(1) .save').simulate('submit');
- MockPromises.tick(1);
- });
-
- it('clears the errors', () => {
- expect(subject.form.state.errors).toEqual({});
- });
- });
- });
-
- describe('when onSubmit throws an error', () => {
- let caught;
-
- beforeEach(() => {
- onSubmit.and.throwError(error);
- subject::setProps({onSubmit});
- try {
- subject.form.onSubmit();
- } catch (e) {
- caught = e;
- }
- });
-
- it('calls the onSubmitError', () => {
- expect(onSubmitError).toHaveBeenCalledWith(error);
- });
-
- it('sets the errors on the state', () => {
- expect(subject.form.state.errors).toBe(errors);
- });
-
- it('re-throws the error', () => {
- expect(caught).toBe(error);
- });
- });
- });
- });
-
- describe('when the field has a validator', () => {
- let validator;
-
- beforeEach(() => {
- validator = jasmine.createSpy('validator');
- subject::setProps({
- children: (
-
-
-
-
-
-
- )
- });
- });
-
- describe('when the validator returns an error', () => {
- beforeEach(() => {
- validator.and.returnValue('some-error');
- $('.form-row:eq(0) .form-col:eq(0) input').val('invalid value').simulate('change');
- $('.form-row:eq(0) .form-col:eq(0) input').simulate('blur');
- });
-
- it('calls the validator', () => {
- expect(validator).toHaveBeenCalledWith('invalid value');
- });
-
- it('renders the error text', () => {
- expect('.form-row:eq(0) .form-col:eq(0) .form-unit').toHaveClass('has-error');
- expect('.form-row:eq(0) .form-col:eq(0) .help-row').toHaveText('some-error');
- });
-
- it('disables the submit button', () => {
- expect('.save').toBeDisabled();
- });
-
- describe('when the validation error is corrected', () => {
- beforeEach(() => {
- validator.and.returnValue(undefined);
- $('.form-row:eq(0) .form-col:eq(0) input').val('valid value').simulate('change');
- });
-
- it('calls the validator', () => {
- expect(validator).toHaveBeenCalledWith('valid value');
- });
-
- it('removes the error text', () => {
- expect('.form-row:eq(0) .form-col:eq(0) .form-unit').not.toHaveClass('has-error');
- expect('.form-row:eq(0) .form-col:eq(0) .help-row').toHaveText('');
- });
-
- it('enables the submit button', () => {
- expect('.save').not.toBeDisabled();
- });
- });
- });
-
- describe('when the validator returns nothing', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(0) input').simulate('blur');
- });
-
- it('calls the validator', () => {
- expect(validator).toHaveBeenCalledWith('some-name');
- });
-
- it('does not render error text', () => {
- expect('.form-row:eq(0) .form-col:eq(0) .form-unit').not.toHaveClass('has-error');
- expect('.form-row:eq(0) .form-col:eq(0) .help-row').toHaveText('');
- });
- });
- });
- });
-
- describe('with two required fields', () => {
- beforeEach(() => {
- subject = ReactDOM.render(
-
-
-
-
-
-
-
-
-
- {Buttons}
-
-
- , root);
- });
-
- it('renders inputs without values', () => {
- expect('.form-row:eq(0) .form-col:eq(0) input').toHaveAttr('value', undefined);
- expect('.form-row:eq(0) .form-col:eq(1) input').toHaveAttr('value', undefined);
- });
-
- it('renders disabled buttons in a col-fixed col', () => {
- expect('.form-row:eq(0) .form-col:eq(2)').toHaveClass('col-fixed');
- expect('.form-row:eq(0) .form-col:eq(2) .save').toHaveAttr('type', 'submit');
- expect('.form-row:eq(0) .form-col:eq(2) .save').toHaveText('Save');
- expect('.form-row:eq(0) .form-col:eq(2) .save').toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(2) .cancel').toHaveText('Cancel');
- expect('.form-row:eq(0) .form-col:eq(2) .cancel').toBeDisabled();
- });
-
- describe('when setting the name', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(0) input').val('some-other-name').simulate('change');
- });
-
- it('allows the name to change', () => {
- expect('.form-row:eq(0) .form-col:eq(0) input').toHaveAttr('value', 'some-other-name');
- });
-
- it('renders buttons ', () => {
- expect('.form-row:eq(0) .form-col:eq(2)').toHaveClass('col-fixed');
- expect('.form-row:eq(0) .form-col:eq(2) .save').toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(2) .cancel').not.toBeDisabled();
- });
-
- describe('when setting the password', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(1) input').val('some-password').simulate('change');
- });
-
- it('renders enabled buttons ', () => {
- expect('.form-row:eq(0) .form-col:eq(2)').toHaveClass('col-fixed');
- expect('.form-row:eq(0) .form-col:eq(2) .save').not.toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(2) .cancel').not.toBeDisabled();
- });
- });
- });
- });
-
- describe('with one optional field', () => {
- beforeEach(() => {
- subject = ReactDOM.render(
-
-
-
-
-
-
- {Buttons}
-
-
- , root);
- });
-
- it('renders an optional text for the input', () => {
- expect('.form-row:eq(0) .form-col:eq(0) .label-row').toHaveText('Some label(Optional)');
- });
-
- it('renders disabled buttons in a col-fixed col', () => {
- expect('.form-row .form-col:eq(1)').toHaveClass('col-fixed');
- expect('.form-row .form-col:eq(1) .save').toHaveAttr('type', 'submit');
- expect('.form-row .form-col:eq(1) .save').toHaveText('Save');
- expect('.form-row .form-col:eq(1) .save').toBeDisabled();
- expect('.form-row .form-col:eq(1) .cancel').toHaveText('Cancel');
- expect('.form-row .form-col:eq(1) .cancel').toBeDisabled();
- });
-
- describe('when changing the optional field', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(0) input').val('some-other-name').simulate('change');
- });
-
- it('renders enabled buttons in a col-fixed col', () => {
- expect('.form-row:eq(0) .form-col:eq(1) .save').not.toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(1) .cancel').not.toBeDisabled();
- });
- });
-
- describe('when deleting the optional field', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(0) input').val('').simulate('change');
- });
-
- it('renders enabled buttons in a col-fixed col', () => {
- expect('.form-row:eq(0) .form-col:eq(1) .save').not.toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(1) .cancel').not.toBeDisabled();
- });
- });
- });
-
- describe('with one required and one optional field', () => {
- beforeEach(() => {
- subject = ReactDOM.render(
-
-
-
-
-
-
-
-
- {Buttons}
-
- , root);
- });
-
- it('renders disabled buttons in a col-fixed col', () => {
- expect('.form-row:eq(0) .form-col:eq(2)').toHaveClass('col-fixed');
- expect('.form-row:eq(0) .form-col:eq(2) .save').toHaveAttr('type', 'submit');
- expect('.form-row:eq(0) .form-col:eq(2) .save').toHaveText('Save');
- expect('.form-row:eq(0) .form-col:eq(2) .save').toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(2) .cancel').toHaveText('Cancel');
- expect('.form-row:eq(0) .form-col:eq(2) .cancel').toBeDisabled();
- });
-
- describe('when changing the optional field', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(1) input').val('some-other-password').simulate('change');
- });
-
- it('renders enabled buttons in a col-fixed col', () => {
- expect('.form-row:eq(0) .form-col:eq(2) .save').not.toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(2) .cancel').not.toBeDisabled();
- });
- });
-
- describe('when deleting the optional field', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(1) input').val('').simulate('change');
- });
-
- it('renders enabled buttons in a col-fixed col', () => {
- expect('.form-row:eq(0) .form-col:eq(2) .save').not.toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(2) .cancel').not.toBeDisabled();
- });
- });
-
- describe('when changing the required field', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(0) input').val('some-other-name').simulate('change');
- });
-
- it('renders enabled buttons in a col-fixed col', () => {
- expect('.form-row:eq(0) .form-col:eq(2) .save').not.toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(2) .cancel').not.toBeDisabled();
- });
- });
-
- describe('when deleting the required field', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(0) input').val('').simulate('change');
- });
-
- it('renders buttons in a col-fixed col', () => {
- expect('.form-row:eq(0) .form-col:eq(2) .save').toBeDisabled();
- expect('.form-row:eq(0) .form-col:eq(2) .cancel').not.toBeDisabled();
- });
- });
- });
-
- describe('with two checkbox fields', () => {
- beforeEach(() => {
- subject = ReactDOM.render(
-
-
-
-
-
-
-
-
-
- , root);
- });
-
- it('sets the initial state correctly', () => {
- expect(subject.form.state.initial).toEqual({
- check1: false,
- check2: ''
- });
- });
-
- it('sets the current state correctly', () => {
- expect(subject.form.state.current).toEqual({
- check1: false,
- check2: ''
- });
- });
- });
-
- describe('with two nameless fields', () => {
- let onChange;
-
- beforeEach(() => {
- onChange = jasmine.createSpy('onChange');
-
- subject = ReactDOM.render(
-
-
-
-
-
-
-
-
-
- , root);
- });
-
- it('does not store their values in the state', () => {
- expect(subject.form.state.initial).toEqual({});
- });
-
- describe('when one field is updated', () => {
- beforeEach(() => {
- $('.field1').val('hello').simulate('change');
- });
-
- it('does not update the state', () => {
- expect(subject.form.state.initial).toEqual({});
- });
-
- it('does not update the other', () => {
- expect($('.field2').val()).toEqual('');
- });
-
- it('retains the value in the input', () => {
- expect($('.field1').val()).toEqual('hello');
- });
-
- it('calls the given onChange callback', () => {
- expect(onChange).toHaveBeenCalledWith(jasmine.any(Object));
- });
- });
- });
-
- describe('with a hidden field', () => {
- beforeEach(() => {
- subject = ReactDOM.render(
-
-
-
-
-
-
- , root);
- });
-
- it('renders the col with the hidden attribute', () => {
- expect('.form-row:eq(0) .form-col:eq(0)').toHaveAttr('hidden');
- });
- });
-
- describe('with a row wrapper', () => {
- let wrapper;
-
- beforeEach(() => {
- wrapper = jasmine.createSpy('wrapper').and.returnValue();
- subject = ReactDOM.render(
-
-
-
-
-
-
- , root);
- });
-
- it('calls the wrapper with the form state', () => {
- expect(wrapper).toHaveBeenCalledWith(subject.form.state);
- });
-
- it('renders the row grid within the wrapper', () => {
- expect('.some-form > fieldset > .wrapper > .grid > .col input').toHaveClass('some-input');
- });
- });
-
- describe('canSubmit with a custom checkRequiredFields callback ', () => {
- let checkRequiredFields;
-
- beforeEach(() => {
- checkRequiredFields = jasmine.createSpy('checkRequiredFields');
- Buttons.and.callFake(({canSubmit}) => (
- Save
- ));
-
- subject = ReactDOM.render(
-
-
-
-
-
-
- {Buttons}
-
-
- , root);
- $('input').val('some-name').simulate('change');
- });
-
- describe('when checking required fields returns true', () => {
- beforeEach(() => {
- checkRequiredFields.and.returnValue(true);
- subject.forceUpdate();
- });
-
- it('renders an enabled save button', () => {
- expect('.save').not.toBeDisabled();
- });
- });
-
- describe('when checking required fields returns false', () => {
- beforeEach(() => {
- checkRequiredFields.and.returnValue(false);
- subject.forceUpdate();
- });
-
- it('renders a disabled save button', () => {
- expect('.save').toBeDisabled();
- });
- });
- });
-
- describe('onModified', () => {
- let onModified;
-
- beforeEach(() => {
- onModified = jasmine.createSpy('onModified');
-
- subject = ReactDOM.render(
-
-
-
-
-
-
- {Buttons}
-
-
- , root);
- });
-
- describe('when modifying', () => {
- beforeEach(() => {
- $('.form-row:eq(0) .form-col:eq(0) input').val('some-other-name').simulate('change');
- });
-
- it('calls the onModified callback with true', () => {
- expect(onModified).toHaveBeenCalledWith(true);
- expect(onModified).not.toHaveBeenCalledWith(false);
- });
-
- describe('when resetting manually', () => {
- beforeEach(() => {
- onModified.calls.reset();
- $('.form-row:eq(0) .form-col:eq(0) input').val('some-name').simulate('change');
- });
-
- it('calls the onModified callback with false', () => {
- expect(onModified).toHaveBeenCalledWith(false);
- expect(onModified).not.toHaveBeenCalledWith(true);
- });
- });
-
- describe('when resetting with the reset callback', () => {
- beforeEach(() => {
- onModified.calls.reset();
- $('.cancel').click();
- });
-
- it('calls the onModified callback with false', () => {
- expect(onModified).toHaveBeenCalledWith(false);
- expect(onModified).not.toHaveBeenCalledWith(true);
- });
- });
-
- describe('when submitting', () => {
- beforeEach(() => {
- onModified.calls.reset();
- $('.save').click();
- MockPromises.tick();
- });
-
- it('calls the onModified callback with false', () => {
- expect(onModified).toHaveBeenCalledWith(false);
- expect(onModified).not.toHaveBeenCalledWith(true);
- });
- });
-
- describe('when unmounting', () => {
- beforeEach(() => {
- onModified.calls.reset();
- ReactDOM.unmountComponentAtNode(root);
- });
-
- it('calls the onModified callback with false', () => {
- expect(onModified).toHaveBeenCalledWith(false);
- expect(onModified).not.toHaveBeenCalledWith(true);
- });
- });
- });
- });
-
- describe('resetOnSubmit', () => {
- beforeEach(() => {
- ReactDOM.render(
-
-
-
-
-
-
-
- , root);
- $('.form-row:eq(0) .form-col:eq(0) input').val('some-other-name').simulate('change');
- $('.form-row:eq(0) .form-col:eq(1) .save').simulate('submit');
- });
-
- it('resets the form to its initial state', () => {
- MockPromises.tick();
- expect($('.form-row:eq(0) .form-col:eq(0) input').val()).toEqual('some-name');
- });
- });
-
- describe('when passed extra props', () => {
- beforeEach(() => {
- ReactDOM.render(
- , root);
- });
-
- it('passes them to the form tag', () => {
- expect('.some-form').toHaveAttr('id', 'some-id');
- expect('.some-form').toHaveAttr('name', 'some-name');
- expect('.some-form').toHaveAttr('method', 'some-method');
- });
- });
-
- describe('when rendering a Page component', () => {
- beforeEach(() => {
- subject = ReactDOM.render(, root);
- $('.form-row:eq(0) .form-col:eq(0) input').val('some-name').simulate('change');
- });
-
- it('renders inputs without values', () => {
- expect('.form-row:eq(0) .form-col').toHaveLength(2);
- expect('.form-row:eq(0) .form-col:eq(0) input').toHaveAttr('value', 'some-name');
- expect('.form-row:eq(0) .form-col:eq(1) input').toHaveAttr('value', undefined);
- });
-
- it('sets the form state', () => {
- expect(subject.gridForm.form.state).toEqual({
- submitting: false,
- errors: {},
- initial: {name: '', other: ''},
- current: {name: 'some-name', other: ''},
- requiredFields: ['name', 'other']
- });
- });
-
- describe('when adding a new col', () => {
- beforeEach(() => {
- $('.col-toggle input').click();
- });
-
- it('renders the new col', () => {
- expect('.form-row:eq(0) .form-col').toHaveLength(3);
- expect('.form-row:eq(0) .form-col:eq(0) input').toHaveAttr('value', 'some-name');
- expect('.form-row:eq(0) .form-col:eq(1) input').toHaveAttr('value', undefined);
- expect('.form-row:eq(0) .form-col:eq(2) input').toHaveAttr('value', undefined);
- });
-
- it('updates the form state', () => {
- expect(subject.gridForm.form.state).toEqual({
- submitting: false,
- errors: {},
- initial: {name: '', password: '', other: ''},
- current: {name: 'some-name', password: '', other: ''},
- requiredFields: ['name', 'password', 'other']
- });
- });
- });
- });
-
- describe('when passing the onChange', () => {
- let subject;
-
- describe('when passed an event', () => {
- beforeEach(() => {
- subject = ReactDOM.render(
- , root);
- $('.form input').val('mytitle').simulate('change');
- });
-
- it('parses the value from the event', () => {
- expect(subject.form.state.current).toEqual({title: 'mytitle'});
- });
- });
-
- describe('when passed a value', () => {
- beforeEach(() => {
- const Component = ({onChange}) => onChange('some-title')}}/>;
- Component.propTypes = {onChange: PropTypes.func};
- subject = ReactDOM.render(
- , root);
- $('.form input').val('').simulate('change');
- });
-
- it('uses the value', () => {
- expect(subject.form.state.current).toEqual({title: 'some-title'});
- });
- });
- });
-});
\ No newline at end of file
diff --git a/src/react/forms/form-col.js b/src/react/forms/form-col.js
deleted file mode 100644
index 50555a1a4..000000000
--- a/src/react/forms/form-col.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import {FlexCol} from '../flex-grids';
-import classnames from 'classnames';
-
-export class FormCol extends React.Component {
- static propTypes = {
- state: PropTypes.object,
- setState: PropTypes.func,
- canSubmit: PropTypes.func,
- onSubmit: PropTypes.func,
- canReset: PropTypes.func,
- reset: PropTypes.func,
- hidden: PropTypes.bool,
- fixed: PropTypes.bool
- };
-
- static defaultProps = {state: {}};
-
- componentDidMount() {
- require('../../css/forms');
- }
-
- render() {
- const {state, setState, canSubmit, onSubmit, canReset, reset, fixed, children, hidden, className, id} = this.props;
- return (
-
- {typeof children === 'function'
- ? children({canSubmit, canReset, reset, onSubmit, submitting: state.submitting, setState, state})
- : children}
-
- );
- }
-}
diff --git a/src/react/forms/form-row.js b/src/react/forms/form-row.js
deleted file mode 100644
index ead984400..000000000
--- a/src/react/forms/form-row.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import classnames from 'classnames';
-import {Grid} from '../flex-grids';
-
-export class FormRow extends React.Component {
- static propTypes = {
- state: PropTypes.object,
- children: PropTypes.node,
- className: PropTypes.string,
- id: PropTypes.string,
- wrapper: PropTypes.func
- };
-
- componentDidMount() {
- require('../../css/forms');
- }
-
- render() {
- const {state, children, className, id, wrapper} = this.props;
- const row = ({children});
- return wrapper ? React.cloneElement(wrapper(state), {children: row}) : row;
- }
-}
\ No newline at end of file
diff --git a/src/react/forms/form-unit.js b/src/react/forms/form-unit.js
index 42982b09a..e7caf5c48 100644
--- a/src/react/forms/form-unit.js
+++ b/src/react/forms/form-unit.js
@@ -21,34 +21,35 @@ export class FormUnit extends React.Component {
tooltip: PropTypes.node,
tooltipSize: PropTypes.oneOf(['sm', 'md', 'lg']),
tooltipPlacement: PropTypes.oneOf(['left', 'right', 'bottom', 'top']),
- field: PropTypes.node,
+ children: PropTypes.node,
help: PropTypes.node,
hasError: PropTypes.bool,
state: PropTypes.object,
- setState: PropTypes.func,
+ setValues: PropTypes.func,
fieldRowClassName: PropTypes.string,
labelRowClassName: PropTypes.string
};
+ static defaultProps = {
+ tooltipSize: 'lg',
+ tooltipPlacement: 'top'
+ };
+
componentDidMount() {
require('../../css/forms');
}
- render() {
- const {
- className, hideHelpRow, retainLabelHeight, inline, label, labelClassName, labelPosition, optional, optionalText,
- tooltip, tooltipSize = 'lg', tooltipPlacement = 'top', field, help, hasError, labelFor, postLabel, state,
- setState, fieldRowClassName, labelRowClassName
- } = this.props;
-
- if (!label && !field && !help) return null;
-
- const tooltipIcon = tooltip &&
+ newTooltipIcon = () => {
+ const {tooltip, tooltipSize, tooltipPlacement} = this.props;
+ return tooltip &&
;
+ };
- const labelElement = (
+ newLabelElement = tooltipIcon => {
+ const {labelClassName, labelFor, label, optional, optionalText} = this.props;
+ return (
);
+ };
- const labelRow = (label || retainLabelHeight || postLabel) && (
- inline
- ? labelElement
- : (
+ newLabelRow = () => {
+ const {label, retainLabelHeight, postLabel, inline, labelRowClassName, state, setValues} = this.props;
+ const tooltipIcon = this.newTooltipIcon();
+ const labelElement = this.newLabelElement(tooltipIcon);
+ return (label || retainLabelHeight || postLabel) && (
+ inline
+ ? labelElement
+ : (
{labelElement}
- {typeof postLabel === 'function' ? postLabel({state, setState}) : postLabel}
+ {typeof postLabel === 'function' ? postLabel({state, setValues}) : postLabel}
)
- );
+ );
+ };
- const fieldRow = field && (inline
- ? field
- : {field}
);
- const helpRowClassName = classnames('help-row', {'type-dark-5': !hasError});
- const helpRow = inline ? help : {help}
;
+ newFieldRow = () => {
+ const {children, inline, fieldRowClassName} = this.props;
+ return children && (inline
+ ? children
+ : {children}
);
+ };
+ newContent = (labelRow, fieldRow, helpRow) => {
+ const {inline, labelRowClassName, labelPosition, fieldRowClassName, hideHelpRow} = this.props;
const sections = labelPosition === 'after' ? [fieldRow, labelRow] : [labelRow, fieldRow];
-
+ const showRowClassNames = (key, position) => key === position && labelPosition !== 'after' || key === (1 - position) && labelPosition === 'after';
const content = inline ? ([
- {sections.map((col, key) => ({col}))}
+ {sections.map((col, key) => (
+ {col}
+ ))}
]
) : sections;
+ !hideHelpRow && content.push(helpRow);
+ return content;
+ };
- if (!hideHelpRow) {
- if (inline) {
- content.push((
-
-
- {helpRow}
-
-
- ));
- } else {
- content.push(helpRow);
- }
+ newHelpRow = () => {
+ const {inline, hasError, help} = this.props;
+ const helpRowClassName = classnames('help-row', {'type-dark-5': !hasError});
+ if (inline) {
+ return (
+
+ {help}
+
+ );
}
+ return {help}
;
+ };
+
+ render() {
+ const {className, inline, label, children, help, hasError} = this.props;
+
+ if (!label && !children && !help) return null;
+
+ const labelRow = this.newLabelRow();
+ const fieldRow = this.newFieldRow();
+ const helpRow = this.newHelpRow();
return (
- {content}
+ {this.newContent(labelRow, fieldRow, helpRow)}
);
}
diff --git a/src/react/forms/form.js b/src/react/forms/form.js
index 019bb5070..10458bb24 100644
--- a/src/react/forms/form.js
+++ b/src/react/forms/form.js
@@ -8,19 +8,21 @@ import {Input} from '../inputs';
import crypto from 'crypto';
const deepClone = o => JSON.parse(JSON.stringify(o));
+// eslint-disable-next-line no-unused-vars
+const getFieldEntries = fields => Object.entries(fields).filter(([name, props]) => props);
+const isOptional = ({optional}, current) => typeof optional === 'function' ? optional({current}) : optional;
const isPromise = promise => promise && typeof promise.then === 'function';
const newId = () => crypto.randomBytes(16).toString('base64');
const noop = () => undefined;
-const newFormState = (fields, cb) => Object.entries(fields)
- .filter(([name, props]) => props)
+const newFormState = (fields, cb) => getFieldEntries(fields)
.reduce((memo, [name, props]) => {
- const {initialValue, currentValue, isRequired} = cb({...props, name});
+ const {initialValue, currentValue} = cb({...props, name});
memo.initial[name] = initialValue;
memo.current[name] = currentValue;
- isRequired && memo.requiredFields.push(name);
return memo;
- }, {initial: {}, current: {}, requiredFields: [], submitting: false, errors: {}});
+ }, {initial: {}, current: {}, submitting: false, errors: {}});
+const newInitialValue = initialValue => [null, undefined].includes(initialValue) ? '' : initialValue;
export class Form extends React.Component {
static propTypes = {
@@ -43,31 +45,29 @@ export class Form extends React.Component {
constructor(props) {
super(props);
- this.state = newFormState(props.fields, ({optional, initialValue}) => {
- initialValue = typeof initialValue === 'undefined' ? '' : initialValue;
- return {isRequired: !optional, initialValue, currentValue: deepClone(initialValue)};
+ this.state = newFormState(props.fields, ({initialValue}) => {
+ initialValue = newInitialValue(initialValue);
+ return {initialValue, currentValue: deepClone(initialValue)};
});
this.setState = this.setState.bind(this);
}
- componentDidMount() {
- require('../../css/forms');
- }
+ // componentDidMount() {
+ // require('../../css/forms');
+ // }
shouldComponentUpdate({fields}, nextState) {
const {current, initial} = nextState;
- const {initial: newInitial, current: newCurrent, requiredFields} = newFormState(fields,
- ({name, optional, initialValue}) => {
- initialValue = typeof initialValue === 'undefined' ? '' : initialValue;
+ const {initial: newInitial, current: newCurrent} = newFormState(fields,
+ ({name, initialValue}) => {
+ initialValue = newInitialValue(initialValue);
return {
- isRequired: !optional,
initialValue: initial.hasOwnProperty(name) ? initial[name] : initialValue,
currentValue: current.hasOwnProperty(name) ? current[name] : deepClone(initialValue)
};
});
nextState.initial = newInitial;
nextState.current = newCurrent;
- nextState.requiredFields = requiredFields;
return true;
}
@@ -75,9 +75,12 @@ export class Form extends React.Component {
this.props.onModified(false);
}
- onChangeCheckbox = name => () => this.setState({current: {...this.state.current, [name]: !this.state.current[name]}});
+ onChangeCheckbox = (name, cb = noop) => val => {
+ if (typeof val.persist === 'function') val.persist();
+ this.setValues({[name]: !this.state.current[name]}, () => cb(val));
+ };
- onChange = (name, validator) => val => {
+ onChange = (name, validator, cb = noop) => val => {
const {initial} = this.state;
const {onModified} = this.props;
const value = val.target && 'value' in val.target ? val.target.value : val;
@@ -87,7 +90,8 @@ export class Form extends React.Component {
nextState.errors = {...this.state.errors};
delete nextState.errors[name];
}
- this.setState(nextState);
+ if (typeof val.persist === 'function') val.persist();
+ this.setState(nextState, () => cb(val));
onModified(!deepEqual(initial, nextState.current));
};
@@ -107,13 +111,14 @@ export class Form extends React.Component {
canSubmit = ({checkRequiredFields} = {}) => {
const {fields} = this.props;
- const {initial, current, submitting, requiredFields} = this.state;
+ const {initial, current, submitting} = this.state;
return !submitting
&& find(Object.keys(initial), key => !deepEqual(initial[key], current[key]))
&& (checkRequiredFields
? checkRequiredFields(this.state.current)
- : !find(requiredFields, key => !current[key]))
- && !find(Object.entries(fields), ([name, {validator}]) => validator && validator(this.state.current[name]));
+ : !find(Object.keys(fields)
+ .filter(name => fields[name] && !isOptional(fields[name], current)), name => !current[name]))
+ && !find(getFieldEntries(fields), ([name, {validator}]) => validator && validator(this.state.current[name]));
};
onSubmit = e => {
@@ -130,7 +135,7 @@ export class Form extends React.Component {
initial: resetOnSubmit ? initial : deepClone(current),
errors: {}
});
- const after = () => afterSubmit({state: this.state, setState: this.setState, response, reset: this.reset});
+ const after = () => afterSubmit({state: this.state, response, reset: this.reset});
const onModifiedPromise = onModified(false);
return isPromise(onModifiedPromise) ? onModifiedPromise.then(after) : after();
};
@@ -152,13 +157,17 @@ export class Form extends React.Component {
}
};
- controlField = ({children = , validator, name}) => {
- const {canSubmit, canReset, reset, onSubmit, setState, state, onChange, onBlur, onChangeCheckbox} = this;
+ setValues = (values, cb) => this.setState({current: {...this.state.current, ...values}}, cb);
+
+ controlField = ({children = , validator, name}) => {
+ const {canSubmit, canReset, reset, onSubmit, setValues, state, onChange, onBlur, onChangeCheckbox} = this;
const {submitting} = state;
- const element = typeof children !== 'function' ? children : children({
- canSubmit, canReset, reset, onSubmit, submitting, setState, state, onChange: onChange(name, validator)
- });
+ const element = typeof children !== 'function'
+ ? children
+ : children({
+ onChange: onChange(name, validator), canSubmit, canReset, reset, onSubmit, submitting, setValues, state
+ });
if (!element || React.Children.count(element) !== 1 || !name) return element;
@@ -166,10 +175,10 @@ export class Form extends React.Component {
if (element.props.type === 'checkbox') {
props.checked = !!(element.props.hasOwnProperty('checked') ? element.props.checked : (state.current && state.current[name]));
- props.onChange = element.props.onChange || onChangeCheckbox(name);
+ props.onChange = onChangeCheckbox(name, element.props.onChange);
} else {
props.value = element.props.hasOwnProperty('value') ? element.props.value : (state.current && state.current[name]);
- props.onChange = element.props.onChange || onChange(name, validator);
+ props.onChange = onChange(name, validator, element.props.onChange);
if (validator) props.onBlur = onBlur({name, validator});
}
@@ -177,22 +186,32 @@ export class Form extends React.Component {
};
render() {
+ // eslint-disable-next-line no-unused-vars
const {className, children, fields, onModified, onSubmitError, afterSubmit, resetOnSubmit, ...others} = this.props;
- const {canSubmit, canReset, reset, onSubmit, setState, state, onChange, onBlur, onChangeCheckbox} = this;
+ const {canSubmit, canReset, reset, onSubmit, setValues, state, onBlur} = this;
+ const {current, submitting} = state;
- const formUnits = Object.entries(fields).reduce((memo, [name, props]) => {
+ const formUnits = getFieldEntries(fields).reduce((memo, [name, props]) => {
const error = state.errors[name];
- const field = this.controlField({...props, name});
+ const children = this.controlField({...props, name});
const help = error || props.help;
- const labelFor = props.labelFor || field.props.id;
- return {...memo, [name]: };
+ const labelFor = props.labelFor || children.props.id;
+ // eslint-disable-next-line no-unused-vars
+ const {className: _, ...rest} = props;
+ const formUnit = (
+
+ );
+ return {...memo, [name]: formUnit};
}, {});
return (
diff --git a/src/react/forms/grid-form.js b/src/react/forms/grid-form.js
deleted file mode 100644
index eff38a96d..000000000
--- a/src/react/forms/grid-form.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import {Form} from './form';
-
-const hasName = col => col && col.props && col.props.name;
-const getFields = children => React.Children.toArray(children).filter(Boolean).reduce((memo, {props: {children}}) => ({
- ...memo,
- ...React.Children.toArray(children).filter(hasName).reduce((memo, {props}) => ({...memo, [props.name]: props}), {})
-}), {});
-
-export class GridForm extends React.Component {
- static propTypes = {
- onModified: PropTypes.func,
- onSubmit: PropTypes.func,
- onSubmitError: PropTypes.func,
- afterSubmit: PropTypes.func,
- resetOnSubmit: PropTypes.bool
- };
-
- constructor(props) {
- super(props);
- this.state = {fields: getFields(props.children)};
- }
-
- shouldComponentUpdate({children}, nextState) {
- nextState.fields = getFields(children);
- return true;
- }
-
- render() {
- const {children, ...others} = this.props;
- return (
-
- );
- }
-}
\ No newline at end of file
diff --git a/src/react/forms/index.js b/src/react/forms/index.js
index aed6e0799..0abe3f59d 100644
--- a/src/react/forms/index.js
+++ b/src/react/forms/index.js
@@ -1,5 +1,2 @@
-export {GridForm} from './grid-form';
-export {FormRow} from './form-row';
-export {FormCol} from './form-col';
export {FormUnit} from './form-unit';
export {Form} from './form';
\ No newline at end of file