Merged in OS-#10-form-validations (pull request #13)
OS-#10 form validations
This commit is contained in:
commit
220ff697f8
|
@ -33,8 +33,8 @@ let MainHomePageLoginWidget = React.createClass({
|
|||
<Widget className="main-home-page--widget" title="Login">
|
||||
<Form className="login-widget--form" onSubmit={this.handleLoginFormSubmit}>
|
||||
<div className="login-widget--inputs">
|
||||
<Input placeholder="email" name="email" className="login-widget--input"/>
|
||||
<Input placeholder="password" name="password" className="login-widget--input" password/>
|
||||
<Input placeholder="email" name="email" className="login-widget--input" validation="EMAIL" required/>
|
||||
<Input placeholder="password" name="password" className="login-widget--input" password required/>
|
||||
<Checkbox name="remember" label="Remember Me" className="login-widget--input"/>
|
||||
</div>
|
||||
<div className="login-widget--submit-button">
|
||||
|
@ -53,7 +53,7 @@ let MainHomePageLoginWidget = React.createClass({
|
|||
<Widget className="main-home-page--widget main-home-page--password-widget" title="Password Recovery">
|
||||
<Form className="login-widget--form" onSubmit={this.handleSubmit}>
|
||||
<div className="login-widget--inputs">
|
||||
<Input placeholder="email" name="email" className="login-widget--input"/>
|
||||
<Input placeholder="email" name="email" className="login-widget--input" validation="EMAIL"/>
|
||||
</div>
|
||||
<div className="login-widget--submit-button">
|
||||
<Button type="primary">Recover my password</Button>
|
||||
|
|
|
@ -1,57 +1,146 @@
|
|||
const Form = require('core-components/form');
|
||||
const Input = require('core-components/input');
|
||||
// MOCKS
|
||||
const ValidationFactoryMock = require('lib-app/__mocks__/validations/validation-factory-mock');
|
||||
const Input = ReactMock();
|
||||
|
||||
// COMPONENTS
|
||||
const Form = requireUnit('core-components/form', {
|
||||
'lib-app/validations/validations-factory': ValidationFactoryMock,
|
||||
'core-components/input': Input
|
||||
});
|
||||
|
||||
describe('Form component', function () {
|
||||
let form, inputs, onSubmit = stub();
|
||||
let form, fields, onSubmit = stub();
|
||||
|
||||
beforeEach(function () {
|
||||
function renderForm() {
|
||||
form = TestUtils.renderIntoDocument(
|
||||
<Form onSubmit={onSubmit}>
|
||||
<div>
|
||||
<Input name="first" value="value1"/>
|
||||
<Input name="second" value="value2" />
|
||||
<Input name="first" value="value1" required/>
|
||||
<Input name="second" value="value2" required validation="CUSTOM"/>
|
||||
</div>
|
||||
<Input name="third" value="value3" />
|
||||
</Form>
|
||||
);
|
||||
inputs = TestUtils.scryRenderedComponentsWithType(form, Input);
|
||||
});
|
||||
fields = TestUtils.scryRenderedComponentsWithType(form, Input);
|
||||
}
|
||||
|
||||
it('should store input value in form state', function () {
|
||||
expect(form.state.form).to.deep.equal({
|
||||
first: 'value1',
|
||||
second: 'value2',
|
||||
third: 'value3'
|
||||
function resetStubs() {
|
||||
ValidationFactoryMock.validators.defaultValidatorMock.validate = stub();
|
||||
ValidationFactoryMock.validators.customValidatorMock.validate = stub();
|
||||
ValidationFactoryMock.getValidator.reset();
|
||||
onSubmit.reset();
|
||||
}
|
||||
|
||||
describe('when mounting the form', function () {
|
||||
beforeEach(renderForm);
|
||||
afterEach(resetStubs);
|
||||
|
||||
it('should store fields values in form state', function () {
|
||||
expect(form.state.form).to.deep.equal({
|
||||
first: 'value1',
|
||||
second: 'value2',
|
||||
third: 'value3'
|
||||
});
|
||||
});
|
||||
|
||||
it('should set validations for required fields', function () {
|
||||
expect(ValidationFactoryMock.getValidator).to.have.been.calledWith('DEFAULT');
|
||||
expect(ValidationFactoryMock.getValidator).to.have.been.calledWith('CUSTOM');
|
||||
expect(ValidationFactoryMock.getValidator).to.have.been.calledTwice;
|
||||
|
||||
expect(form.state.validations).to.deep.equal({
|
||||
first: ValidationFactoryMock.validators.defaultValidatorMock,
|
||||
second: ValidationFactoryMock.validators.customValidatorMock
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update form state if an input value changes', function () {
|
||||
inputs[0].props.onChange({ target: {value: 'value4'}});
|
||||
describe('when interacting with fields', function () {
|
||||
beforeEach(renderForm);
|
||||
afterEach(resetStubs);
|
||||
|
||||
expect(form.state.form).to.deep.equal({
|
||||
first: 'value4',
|
||||
second: 'value2',
|
||||
third: 'value3'
|
||||
it('should update form state if a field value changes', function () {
|
||||
fields[0].props.onChange({ target: {value: 'value4'}});
|
||||
|
||||
expect(form.state.form).to.deep.equal({
|
||||
first: 'value4',
|
||||
second: 'value2',
|
||||
third: 'value3'
|
||||
});
|
||||
});
|
||||
|
||||
it('should update field value if state value changes', function () {
|
||||
form.setState({
|
||||
form: {
|
||||
first: 'value6',
|
||||
second: 'value7',
|
||||
third: 'value8'
|
||||
}
|
||||
});
|
||||
|
||||
expect(fields[0].props.value).to.equal('value6');
|
||||
expect(fields[1].props.value).to.equal('value7');
|
||||
expect(fields[2].props.value).to.equal('value8');
|
||||
});
|
||||
|
||||
it('should validate required fields when blurring', function () {
|
||||
ValidationFactoryMock.validators.defaultValidatorMock.validate = stub().returns('MOCK_ERROR');
|
||||
ValidationFactoryMock.validators.customValidatorMock.validate = stub().returns('MOCK_ERROR_2');
|
||||
expect(fields[0].props.error).to.equal(undefined);
|
||||
expect(fields[0].props.error).to.equal(undefined);
|
||||
expect(fields[0].props.error).to.equal(undefined);
|
||||
|
||||
TestUtils.Simulate.blur(ReactDOM.findDOMNode(fields[0]));
|
||||
expect(fields[0].props.error).to.equal('MOCK_ERROR');
|
||||
|
||||
TestUtils.Simulate.blur(ReactDOM.findDOMNode(fields[1]));
|
||||
expect(fields[1].props.error).to.equal('MOCK_ERROR_2');
|
||||
|
||||
TestUtils.Simulate.blur(ReactDOM.findDOMNode(fields[2]));
|
||||
expect(fields[2].props.error).to.equal(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update input value if state value changes', function () {
|
||||
form.setState({
|
||||
form: {
|
||||
first: 'value6',
|
||||
second: 'value7',
|
||||
third: 'value8'
|
||||
}
|
||||
describe('when submitting the form', function () {
|
||||
beforeEach(renderForm);
|
||||
afterEach(resetStubs);
|
||||
|
||||
it('should call onSubmit callback', function () {
|
||||
TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
|
||||
|
||||
expect(form.props.onSubmit).to.have.been.calledWith(form.state.form);
|
||||
});
|
||||
|
||||
expect(inputs[0].props.value).to.equal('value6');
|
||||
expect(inputs[1].props.value).to.equal('value7');
|
||||
expect(inputs[2].props.value).to.equal('value8');
|
||||
});
|
||||
it('should validate all fields and not call onSubmit if there are errors', function () {
|
||||
ValidationFactoryMock.validators.defaultValidatorMock.validate = stub().returns('MOCK_ERROR');
|
||||
ValidationFactoryMock.validators.customValidatorMock.validate = stub().returns('MOCK_ERROR_2');
|
||||
fields[0].focus = spy(fields[0].focus);
|
||||
fields[1].focus = spy(fields[1].focus);
|
||||
|
||||
it('should call onSubmit callback when form is submitted', function () {
|
||||
TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
|
||||
TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
|
||||
|
||||
expect(form.props.onSubmit).to.have.been.calledWith(form.state.form);
|
||||
expect(fields[0].props.error).to.equal('MOCK_ERROR');
|
||||
expect(fields[1].props.error).to.equal('MOCK_ERROR_2');
|
||||
expect(fields[2].props.error).to.equal(undefined);
|
||||
expect(form.props.onSubmit).to.not.have.been.called;
|
||||
});
|
||||
|
||||
it('should focus the first field with error', function () {
|
||||
ValidationFactoryMock.validators.defaultValidatorMock.validate = stub().returns('MOCK_ERROR');
|
||||
ValidationFactoryMock.validators.customValidatorMock.validate = stub().returns('MOCK_ERROR_2');
|
||||
fields[0].focus = spy(fields[0].focus);
|
||||
fields[1].focus = spy(fields[1].focus);
|
||||
|
||||
TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
|
||||
expect(fields[0].focus).to.have.been.called;
|
||||
|
||||
ValidationFactoryMock.validators.defaultValidatorMock.validate = stub();
|
||||
fields[0].focus.reset();
|
||||
fields[1].focus.reset();
|
||||
|
||||
TestUtils.Simulate.submit(ReactDOM.findDOMNode(form));
|
||||
expect(fields[0].focus).to.not.have.been.called;
|
||||
expect(fields[1].focus).to.have.been.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
const Input = require('core-components/input');
|
||||
|
||||
describe('Input component', function () {
|
||||
|
||||
let nativeInput, input;
|
||||
|
||||
function renderInput(props) {
|
||||
input = TestUtils.renderIntoDocument(
|
||||
<Input {...props} />
|
||||
);
|
||||
nativeInput = TestUtils.findRenderedDOMComponentWithTag(input, 'input');
|
||||
}
|
||||
|
||||
describe('when passing props that affects the native input', function () {
|
||||
|
||||
it('should render type text if it has not specified type', function () {
|
||||
renderInput();
|
||||
|
||||
expect(nativeInput.getAttribute('type')).to.equal('text');
|
||||
});
|
||||
|
||||
it('should render type password if password pass is passed', function () {
|
||||
renderInput({
|
||||
password: true
|
||||
});
|
||||
|
||||
expect(nativeInput.getAttribute('type')).to.equal('password');
|
||||
});
|
||||
|
||||
it('should render aria-required instead of required', function () {
|
||||
renderInput({
|
||||
required: true
|
||||
});
|
||||
|
||||
expect(nativeInput.getAttribute('required')).to.not.equal('null');
|
||||
expect(nativeInput.getAttribute('aria-required')).to.equal('true');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,42 +1,31 @@
|
|||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
const _ = require('lodash');
|
||||
|
||||
import {reactDFS, renderChildrenWithProps} from 'lib-core/react-dfs';
|
||||
const {reactDFS, renderChildrenWithProps} = require('lib-core/react-dfs');
|
||||
const ValidationFactory = require('lib-app/validations/validations-factory');
|
||||
|
||||
import Input from 'core-components/input';
|
||||
import Checkbox from 'core-components/checkbox';
|
||||
const Input = require('core-components/input');
|
||||
const Checkbox = require('core-components/checkbox');
|
||||
|
||||
let Form = React.createClass({
|
||||
|
||||
validations: {},
|
||||
const Form = React.createClass({
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
form: {}
|
||||
}
|
||||
form: {},
|
||||
validations: {},
|
||||
errors: {}
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
let formState = {};
|
||||
|
||||
reactDFS(this.props.children, (child) => {
|
||||
if (child.type === Input) {
|
||||
formState[child.props.name] = child.props.value || '';
|
||||
}
|
||||
else if (child.type === Checkbox) {
|
||||
formState[child.props.name] = child.props.checked || false;
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({
|
||||
form: formState
|
||||
});
|
||||
this.setState(this.getInitialFormAndValidations());
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form {...this.getProps()}>
|
||||
{renderChildrenWithProps(this.props.children, this.getInputProps)}
|
||||
{renderChildrenWithProps(this.props.children, this.getFieldProps)}
|
||||
</form>
|
||||
);
|
||||
},
|
||||
|
@ -49,43 +38,129 @@ let Form = React.createClass({
|
|||
return props;
|
||||
},
|
||||
|
||||
getInputProps({props, type}) {
|
||||
getFieldProps({props, type}) {
|
||||
let additionalProps = {};
|
||||
|
||||
if (type === Input || type === Checkbox) {
|
||||
let inputName = props.name;
|
||||
|
||||
this.validations[inputName] = props.validation;
|
||||
let fieldName = props.name;
|
||||
|
||||
additionalProps = {
|
||||
onChange: this.handleInputChange.bind(this, inputName, type),
|
||||
value: this.state.form[inputName] || props.value
|
||||
ref: fieldName,
|
||||
value: this.state.form[fieldName] || props.value,
|
||||
error: this.state.errors[fieldName],
|
||||
onChange: this.handleFieldChange.bind(this, fieldName, type),
|
||||
onBlur: this.validateField.bind(this, fieldName)
|
||||
}
|
||||
}
|
||||
|
||||
return additionalProps;
|
||||
},
|
||||
|
||||
handleSubmit (event) {
|
||||
getFirstErrorField() {
|
||||
let fieldName = _.findKey(this.state.errors);
|
||||
let fieldNode;
|
||||
|
||||
if (fieldName) {
|
||||
fieldNode = this.refs[fieldName];
|
||||
}
|
||||
|
||||
return fieldNode;
|
||||
},
|
||||
|
||||
getAllFieldErrors() {
|
||||
let form = this.state.form;
|
||||
let fields = Object.keys(this.state.form);
|
||||
let errors = {};
|
||||
|
||||
_.each(fields, (fieldName) => {
|
||||
errors = this.getErrorsWithValidatedField(fieldName, form, errors);
|
||||
});
|
||||
|
||||
return errors;
|
||||
},
|
||||
|
||||
getErrorsWithValidatedField(fieldName, form = this.state.form, errors = this.state.errors) {
|
||||
let newErrors = _.clone(errors);
|
||||
|
||||
if (this.state.validations[fieldName]) {
|
||||
newErrors[fieldName] = this.state.validations[fieldName].validate(form[fieldName], form);
|
||||
}
|
||||
|
||||
return newErrors;
|
||||
},
|
||||
|
||||
getInitialFormAndValidations() {
|
||||
let form = {};
|
||||
let validations = {};
|
||||
|
||||
reactDFS(this.props.children, (child) => {
|
||||
|
||||
if (this.isValidFieldType(child)) {
|
||||
if (child.type === Input) {
|
||||
form[child.props.name] = child.props.value || '';
|
||||
}
|
||||
else if (child.type === Checkbox) {
|
||||
form[child.props.name] = child.props.checked || false;
|
||||
}
|
||||
|
||||
if (child.props.required) {
|
||||
validations[child.props.name] = ValidationFactory.getValidator(child.props.validation || 'DEFAULT');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
form: form,
|
||||
validations: validations
|
||||
}
|
||||
},
|
||||
|
||||
handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.props.onSubmit) {
|
||||
if (this.hasFormErrors()) {
|
||||
this.setState({
|
||||
errors: this.getAllFieldErrors()
|
||||
}, this.focusFirstErrorField);
|
||||
} else if (this.props.onSubmit) {
|
||||
this.props.onSubmit(this.state.form);
|
||||
}
|
||||
},
|
||||
|
||||
handleInputChange(inputName, type, event) {
|
||||
handleFieldChange(fieldName, type, event) {
|
||||
let form = _.clone(this.state.form);
|
||||
|
||||
form[inputName] = event.target.value;
|
||||
form[fieldName] = event.target.value;
|
||||
|
||||
if (type === Checkbox) {
|
||||
form[inputName] = event.target.checked || false;
|
||||
form[fieldName] = event.target.checked || false;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
form: form
|
||||
});
|
||||
},
|
||||
|
||||
isValidFieldType(child) {
|
||||
return child.type === Input || child.type === Checkbox;
|
||||
},
|
||||
|
||||
hasFormErrors() {
|
||||
return _.some(this.getAllFieldErrors());
|
||||
},
|
||||
|
||||
validateField(fieldName) {
|
||||
this.setState({
|
||||
errors: this.getErrorsWithValidatedField(fieldName)
|
||||
});
|
||||
},
|
||||
|
||||
focusFirstErrorField() {
|
||||
let firstErrorField = this.getFirstErrorField();
|
||||
|
||||
if (firstErrorField) {
|
||||
firstErrorField.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
const React = require('react');
|
||||
const classNames = require('classnames');
|
||||
const _ = require('lodash');
|
||||
|
||||
let Input = React.createClass({
|
||||
const Input = React.createClass({
|
||||
|
||||
propTypes: {
|
||||
value: React.PropTypes.string,
|
||||
validation: React.PropTypes.func,
|
||||
validation: React.PropTypes.string,
|
||||
onChange: React.PropTypes.func,
|
||||
inputType: React.PropTypes.string,
|
||||
password: React.PropTypes.bool
|
||||
password: React.PropTypes.bool,
|
||||
required: React.PropTypes.bool
|
||||
},
|
||||
|
||||
getDefaultProps() {
|
||||
|
@ -22,15 +23,18 @@ let Input = React.createClass({
|
|||
return (
|
||||
<label className={this.getClass()}>
|
||||
<span className="input--label">{this.props.label}</span>
|
||||
<input {...this.getProps()} className="input--text" />
|
||||
<input {...this.getInputProps()} className="input--text" />
|
||||
</label>
|
||||
);
|
||||
},
|
||||
|
||||
getProps() {
|
||||
getInputProps() {
|
||||
let props = _.clone(this.props);
|
||||
|
||||
props.required = null;
|
||||
props['aria-required'] = this.props.required;
|
||||
props.type = (this.props.password) ? 'password' : 'text';
|
||||
props.ref = 'nativeInput';
|
||||
|
||||
return props;
|
||||
},
|
||||
|
@ -44,6 +48,12 @@ let Input = React.createClass({
|
|||
};
|
||||
|
||||
return classNames(classes);
|
||||
},
|
||||
|
||||
focus() {
|
||||
if (this.refs.nativeInput) {
|
||||
this.refs.nativeInput.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import keys from 'data/i18n-keys'
|
||||
const englishLanguage = require('data/languages/en');
|
||||
const spanishLanguage = require('data/languages/es');
|
||||
|
||||
let languages = [
|
||||
'us',
|
||||
'es'
|
||||
];
|
||||
const languages = {
|
||||
'us': englishLanguage,
|
||||
'es': spanishLanguage
|
||||
};
|
||||
|
||||
|
||||
let i18nData = function (key, lang) {
|
||||
let langIndex = languages.indexOf(lang);
|
||||
|
||||
return keys[key][langIndex];
|
||||
const i18nData = function (key, lang) {
|
||||
return languages[lang][key];
|
||||
};
|
||||
|
||||
export default i18nData
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
export default {
|
||||
'SUBMIT': ['Submit', 'Enviar'],
|
||||
'LOG_IN': ['Log in', 'Ingresar'],
|
||||
'SIGN_UP': ['Sign up', 'Registrarse']
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
'SUBMIT': 'Submit',
|
||||
'LOG_IN': 'Log in',
|
||||
'SIGN_UP': 'Sign up',
|
||||
'ERROR_EMPTY': 'Invalid value',
|
||||
'ERROR_EMAIL': 'Invalid email'
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
'SUBMIT': 'Enviar',
|
||||
'LOG_IN': 'Ingresar',
|
||||
'SIGN_UP': 'Registrarse',
|
||||
'ERROR_EMPTY': 'Valor invalido',
|
||||
'ERROR_EMAIL': 'Email invalido'
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
let customValidatorMock = {validate: stub()};
|
||||
let defaultValidatorMock = {validate: stub()};
|
||||
|
||||
export default {
|
||||
getValidator: spy(function (validation) {
|
||||
return (validation === 'CUSTOM') ? customValidatorMock : defaultValidatorMock;
|
||||
}),
|
||||
validators: {
|
||||
customValidatorMock: customValidatorMock,
|
||||
defaultValidatorMock: defaultValidatorMock
|
||||
}
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
const Validator = require('lib-app/validations/validator');
|
||||
|
||||
class EmailValidator extends Validator {
|
||||
|
||||
validate(value, form) {
|
||||
if (!value.length) return this.getError('ERROR_EMPTY');
|
||||
if (value.indexOf('@') === -1) return this.getError('ERROR_EMAIL');
|
||||
}
|
||||
}
|
||||
|
||||
export default EmailValidator;
|
|
@ -0,0 +1,16 @@
|
|||
const Validator = require('lib-app/validations/validator');
|
||||
const EmailValidator = require('lib-app/validations/email-validator');
|
||||
|
||||
let validators = {
|
||||
'DEFAULT': new Validator(),
|
||||
'EMAIL': new EmailValidator()
|
||||
};
|
||||
|
||||
class ValidatorFactory {
|
||||
|
||||
static getValidator(validatorKey) {
|
||||
return validators[validatorKey];
|
||||
}
|
||||
}
|
||||
|
||||
export default ValidatorFactory;
|
|
@ -0,0 +1,13 @@
|
|||
const i18n = require('lib-app/i18n');
|
||||
|
||||
class Validator {
|
||||
validate(value, form) {
|
||||
if (!value.length) return this.getError('ERROR_EMPTY');
|
||||
}
|
||||
|
||||
getError(errorKey) {
|
||||
return i18n(errorKey);
|
||||
}
|
||||
}
|
||||
|
||||
export default Validator
|
|
@ -11,6 +11,7 @@ global.chai = require('chai');
|
|||
global.expect = chai.expect;
|
||||
global.sinon = require('sinon');
|
||||
global.stub = sinon.stub;
|
||||
global.spy = sinon.spy;
|
||||
global.proxyquire = require('proxyquire');
|
||||
global.ReactMock = require('lib-test/react-mock');
|
||||
chai.use(require('sinon-chai'));
|
||||
|
|
Loading…
Reference in New Issue