Form validation errors integration for ChangePassword

This commit is contained in:
SleepWalker 2016-05-02 12:20:50 +03:00
parent 5adfc60783
commit 82416f788f
13 changed files with 186 additions and 90 deletions

View File

@ -184,8 +184,8 @@ class PanelTransition extends Component {
this.body.onFormSubmit();
};
onFormInvalid = (errorMessage) => {
this.props.setError(errorMessage);
onFormInvalid = (errors) => {
this.props.setError(Object.values(errors).shift());
};
willEnter = (config) => this.getTransitionStyles(config);

View File

@ -14,16 +14,23 @@ export default class ChangePassword extends Component {
static displayName = 'ChangePassword';
static propTypes = {
form: PropTypes.instanceOf(FormModel).isRequired,
onSubmit: PropTypes.func.isRequired
};
form = new FormModel();
static get defaultProps() {
return {
form: new FormModel()
};
}
render() {
const {form} = this;
const {form} = this.props;
return (
<Form onSubmit={this.onFormSubmit}>
<Form onSubmit={this.onFormSubmit}
form={form}
>
<div className={styles.contentWithBackButton}>
<Link className={styles.backButton} to="/" />
@ -89,6 +96,6 @@ export default class ChangePassword extends Component {
}
onFormSubmit = () => {
this.props.onSubmit(this.form.serialize());
this.props.onSubmit(this.props.form);
};
}

View File

@ -11,19 +11,22 @@ export default class PasswordRequestForm extends Component {
static displayName = 'PasswordRequestForm';
static propTypes = {
form: PropTypes.instanceOf(FormModel).isRequired,
onSubmit: PropTypes.func.isRequired
};
form = new FormModel();
render() {
const {form} = this.props;
return (
<Form onSubmit={this.onSubmit}>
<Form onSubmit={this.onSubmit}
form={form}
>
<h2>
<Message {...messages.title} />
</h2>
<Input {...this.form.bindField('password')}
<Input {...form.bindField('password')}
type="password"
required
autoFocus
@ -38,6 +41,6 @@ export default class PasswordRequestForm extends Component {
}
onSubmit = () => {
this.props.onSubmit(this.form.value('password'));
this.props.onSubmit(this.props.form);
};
}

View File

@ -1,11 +1,12 @@
import React, { Component, PropTypes } from 'react';
import React, { PropTypes } from 'react';
import classNames from 'classnames';
import { intlShape } from 'react-intl';
import buttons from 'components/ui/buttons.scss';
export default class Button extends Component {
import FormComponent from './FormComponent';
export default class Button extends FormComponent {
static displayName = 'Button';
static propTypes = {
@ -19,10 +20,6 @@ export default class Button extends Component {
color: PropTypes.oneOf(['green', 'blue', 'red', 'lightViolet', 'darkBlue'])
};
static contextTypes = {
intl: intlShape.isRequired
};
render() {
const { color = 'green', block } = this.props;
@ -30,9 +27,7 @@ export default class Button extends Component {
...this.props
};
if (props.label.id) {
props.label = this.context.intl.formatMessage(props.label);
}
props.label = this.formatMessage(props.label);
return (
<button className={classNames(buttons[color], {

View File

@ -1,11 +1,11 @@
import React, { Component, PropTypes } from 'react';
import React, { PropTypes } from 'react';
import classNames from 'classnames';
import { intlShape } from 'react-intl';
import styles from './form.scss';
import FormInputComponent from './FormInputComponent';
export default class Checkbox extends Component {
export default class Checkbox extends FormInputComponent {
static displayName = 'Checkbox';
static propTypes = {
@ -19,16 +19,10 @@ export default class Checkbox extends Component {
]).isRequired
};
static contextTypes = {
intl: intlShape.isRequired
};
render() {
let { label, color = 'green', skin = 'dark' } = this.props;
if (label && label.id) {
label = this.context.intl.formatMessage(label);
}
label = this.formatMessage(label);
return (
<div className={classNames(styles[`${color}CheckboxRow`], styles[`${skin}CheckboxRow`])}>
@ -37,14 +31,11 @@ export default class Checkbox extends Component {
<div className={styles.checkbox} />
{label}
</label>
{this.renderError()}
</div>
);
}
setEl = (el) => {
this.el = el;
};
getValue() {
return this.el.checked ? 1 : 0;
}

View File

@ -2,6 +2,8 @@ import React, { Component, PropTypes } from 'react';
import classNames from 'classnames';
import FormModel from 'models/Form';
import styles from './form.scss';
export default class Form extends Component {
@ -10,6 +12,7 @@ export default class Form extends Component {
static propTypes = {
id: PropTypes.string, // and id, that uniquely identifies form contents
isLoading: PropTypes.bool,
form: PropTypes.instanceOf(FormModel),
onSubmit: PropTypes.func,
onInvalid: PropTypes.func,
children: PropTypes.oneOfType([
@ -71,17 +74,30 @@ export default class Form extends Component {
if (form.checkValidity()) {
this.props.onSubmit();
} else {
const firstError = form.querySelectorAll(':invalid')[0];
firstError.focus();
const invalidEls = form.querySelectorAll(':invalid');
const errors = {};
invalidEls[0].focus(); // focus on first error
let errorMessage = firstError.validationMessage;
if (firstError.validity.valueMissing) {
errorMessage = `error.${firstError.name}_required`;
} else if (firstError.validity.typeMismatch) {
errorMessage = `error.${firstError.name}_invalid`;
}
Array.from(invalidEls).reduce((errors, el) => {
if (!el.name) {
console.warn('Found an element without name', el);
return errors;
}
this.props.onInvalid(errorMessage);
let errorMessage = el.validationMessage;
if (el.validity.valueMissing) {
errorMessage = `error.${el.name}_required`;
} else if (el.validity.typeMismatch) {
errorMessage = `error.${el.name}_invalid`;
}
errors[el.name] = errorMessage;
return errors;
}, errors);
this.props.form && this.props.form.setErrors(errors);
this.props.onInvalid(errors);
}
};
}

View File

@ -0,0 +1,19 @@
import React, { Component } from 'react';
import { intlShape } from 'react-intl';
export default class FormComponent extends Component {
static displayName = 'FormComponent';
static contextTypes = {
intl: intlShape.isRequired
};
formatMessage(message) {
if (message && message.id && this.context && this.context.intl) {
message = this.context.intl.formatMessage(message);
}
return message;
}
}

View File

@ -0,0 +1,44 @@
import React, { PropTypes } from 'react';
import errorsDict from 'services/errorsDict';
import styles from './form.scss';
import FormComponent from './FormComponent';
export default class FormInputComponent extends FormComponent {
static displayName = 'FormInputComponent';
static propTypes = {
error: PropTypes.string
};
componentWillReceiveProps() {
if (this.state && this.state.error) {
Reflect.deleteProperty(this.state, 'error');
this.setState(this.state);
}
}
setEl = (el) => {
this.el = el;
};
renderError() {
const error = this.state && this.state.error || this.props.error;
if (error) {
return (
<div className={styles.fieldError}>
{errorsDict.resolve(error)}
</div>
);
}
return null;
}
setError(error) {
this.setState({error});
}
}

View File

@ -1,14 +1,14 @@
import React, { Component, PropTypes } from 'react';
import React, { PropTypes } from 'react';
import classNames from 'classnames';
import { intlShape } from 'react-intl';
import { uniqueId } from 'functions';
import icons from 'components/ui/icons.scss';
import styles from './form.scss';
import FormInputComponent from './FormInputComponent';
export default class Input extends Component {
export default class Input extends FormInputComponent {
static displayName = 'Input';
static propTypes = {
@ -23,19 +23,15 @@ export default class Input extends Component {
id: PropTypes.string
}),
PropTypes.string
]).isRequired,
]),
error: PropTypes.string,
icon: PropTypes.string,
skin: PropTypes.oneOf(['dark', 'light']),
color: PropTypes.oneOf(['green', 'blue', 'red', 'lightViolet', 'darkBlue'])
};
static contextTypes = {
intl: intlShape.isRequired
};
render() {
let { icon, color = 'green', skin = 'dark', error, label } = this.props;
let { icon, color = 'green', skin = 'dark', label } = this.props;
const props = {
type: 'text',
@ -47,9 +43,7 @@ export default class Input extends Component {
props.id = uniqueId('input');
}
if (label.id) {
label = this.context.intl.formatMessage(label);
}
label = this.formatMessage(label);
label = (
<label className={styles.textFieldLabel} htmlFor={props.id}>
@ -58,9 +52,7 @@ export default class Input extends Component {
);
}
if (props.placeholder && props.placeholder.id) {
props.placeholder = this.context.intl.formatMessage(props.placeholder);
}
props.placeholder = this.formatMessage(props.placeholder);
let baseClass = styles.formRow;
if (icon) {
@ -70,33 +62,24 @@ export default class Input extends Component {
);
}
if (error) {
error = (
<div className={styles.fieldError}>
error
</div>
);
}
return (
<div className={baseClass}>
{label}
<div className={styles.textFieldContainer}>
<input ref={this.setEl} className={classNames(
styles[`${skin}TextField`],
styles[`${color}TextField`]
)} {...props} />
<input ref={this.setEl}
className={classNames(
styles[`${skin}TextField`],
styles[`${color}TextField`]
)}
{...props}
/>
{icon}
</div>
{error}
{this.renderError()}
</div>
);
}
setEl = (el) => {
this.el = el;
};
getValue() {
return this.el.value;
}

View File

@ -1,5 +1,8 @@
import FormInputComponent from 'components/ui/form/FormInputComponent';
export default class Form {
fields = {};
errors = {};
/**
* Connects form with React's component
@ -12,13 +15,22 @@ export default class Form {
* @return {Object} ref and name props for component
*/
bindField(name) {
return {
const props = {
name,
ref: (el) => {
// TODO: validate React component
if (!(el instanceof FormInputComponent)) {
throw new Error('Expected a component from components/ui/form module');
}
this.fields[name] = el;
}
};
if (this.getError(name)) {
props.error = this.getError(name);
}
return props;
}
focus(fieldId) {
@ -37,9 +49,24 @@ export default class Form {
return this.fields[fieldId].getValue();
}
setErrors(errors) {
const oldErrors = this.errors;
this.errors = errors;
Object.keys(this.fields).forEach((fieldId) => {
if (oldErrors[fieldId] || errors[fieldId]) {
this.fields[fieldId].setError(errors[fieldId] || null);
}
});
}
getError(fieldId) {
return this.errors[fieldId] || null;
}
serialize() {
return Object.keys(this.fields).reduce((acc, key) => {
acc[key] = this.fields[key].getValue();
return Object.keys(this.fields).reduce((acc, fieldId) => {
acc[fieldId] = this.fields[fieldId].getValue();
return acc;
}, {});

View File

@ -1,6 +1,7 @@
import React, { Component, PropTypes } from 'react';
import accounts from 'services/api/accounts';
import FormModel from 'models/Form';
import ChangePassword from 'components/profile/changePassword/ChangePassword';
import PasswordRequestForm from 'components/profile/passwordRequestForm/PasswordRequestForm';
@ -11,14 +12,16 @@ class ChangePasswordPage extends Component {
changePassword: PropTypes.func.isRequired
};
form = new FormModel();
render() {
return (
<ChangePassword onSubmit={this.onSubmit} />
<ChangePassword onSubmit={this.onSubmit} form={this.form} />
);
}
onSubmit = (data) => {
this.props.changePassword(data);
onSubmit = () => {
this.props.changePassword(this.form);
};
}
@ -32,16 +35,23 @@ function goToProfile() {
}
export default connect(null, {
changePassword: (data) => {
changePassword: (form) => {
return (dispatch) => {
// TODO: судя по всему registerPopup было явно лишним. Надо еще раз
// обдумать API и переписать
dispatch(registerPopup('requestPassword', PasswordRequestForm));
dispatch(createPopup('requestPassword', (props) => {
return {
onSubmit: (password) => {
form,
onSubmit: () => {
// TODO: hide this logic in action
accounts.changePassword({
...data,
password
accounts.changePassword(form.serialize())
.catch((resp) => {
if (resp.errors) {
form.setErrors(resp.errors);
}
return Promise.reject(resp);
})
.then(() => {
dispatch(updateUser({

View File

@ -8,13 +8,14 @@ export default {
resolve(error) {
return errorsMap[error] ? errorsMap[error]() : error;
}
}
};
const errorsMap = {
'error.login_required': () => <Message {...messages.loginRequired} />,
'error.login_not_exist': () => <Message {...messages.loginNotExist} />,
'error.password_required': () => <Message {...messages.passwordRequired} />,
'error.password_invalid': () => <Message {...messages.invalidPassword} />,
'error.password_incorrect': () => (
<span>
<Message {...messages.invalidPassword} />

View File

@ -3,7 +3,7 @@ import { defineMessages } from 'react-intl';
export default defineMessages({
invalidPassword: {
id: 'invalidPassword',
defaultMessage: 'You entered wrong account password.'
defaultMessage: 'You have entered wrong account password.'
},
suggestResetPassword: {