Add support for validating more strictly at submit time

When submitting a form, we want to validate more strictly to check for empty
values that might be required. A separate mode is used since we want to ignore
this issue when visiting a field one by one to enter data.

As an example, we convert the pre-existing logic for the username requirement
using this new support.
This commit is contained in:
J. Ryan Stinnett 2019-04-18 21:33:37 +01:00
parent a7c37733b8
commit 1cbb4be6f7
5 changed files with 36 additions and 38 deletions

View file

@ -344,12 +344,6 @@ module.exports = React.createClass({
case "RegistrationForm.ERR_MISSING_PHONE_NUMBER": case "RegistrationForm.ERR_MISSING_PHONE_NUMBER":
errMsg = _t('A phone number is required to register on this homeserver.'); errMsg = _t('A phone number is required to register on this homeserver.');
break; break;
case "RegistrationForm.ERR_USERNAME_INVALID":
errMsg = _t("A username can only contain lower case letters, numbers and '=_-./'");
break;
case "RegistrationForm.ERR_USERNAME_BLANK":
errMsg = _t('You need to enter a username.');
break;
default: default:
console.error("Unknown error code: %s", errCode); console.error("Unknown error code: %s", errCode);
errMsg = _t('An unknown error occurred.'); errMsg = _t('An unknown error occurred.');

View file

@ -94,7 +94,6 @@ module.exports = React.createClass({
this.validateField(FIELD_EMAIL, ev.type); this.validateField(FIELD_EMAIL, ev.type);
this.validateField(FIELD_PASSWORD_CONFIRM, ev.type); this.validateField(FIELD_PASSWORD_CONFIRM, ev.type);
this.validateField(FIELD_PASSWORD, ev.type); this.validateField(FIELD_PASSWORD, ev.type);
this.validateField(FIELD_USERNAME, ev.type);
const allFieldsValid = this.verifyFieldsBeforeSubmit(); const allFieldsValid = this.verifyFieldsBeforeSubmit();
if (!allFieldsValid) { if (!allFieldsValid) {
@ -142,23 +141,38 @@ module.exports = React.createClass({
}, },
verifyFieldsBeforeSubmit() { verifyFieldsBeforeSubmit() {
if (this.allFieldsValid()) { const fieldIDsInDisplayOrder = [
return true;
}
const invalidField = this.findFirstInvalidField([
FIELD_USERNAME, FIELD_USERNAME,
FIELD_PASSWORD, FIELD_PASSWORD,
FIELD_PASSWORD_CONFIRM, FIELD_PASSWORD_CONFIRM,
FIELD_EMAIL, FIELD_EMAIL,
FIELD_PHONE_NUMBER, FIELD_PHONE_NUMBER,
]); ];
// Run all fields with stricter validation that no longer allows empty
// values for required fields.
for (const fieldID of fieldIDsInDisplayOrder) {
const field = this[fieldID];
if (!field) {
continue;
}
field.validate({ allowEmpty: false });
}
if (this.allFieldsValid()) {
return true;
}
const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder);
if (!invalidField) { if (!invalidField) {
return true; return true;
} }
// Focus the first invalid field and show feedback in the stricter mode
// that no longer allows empty values for required fields.
invalidField.focus(); invalidField.focus();
invalidField.validate({ allowEmpty: false, focused: true });
return false; return false;
}, },
@ -215,21 +229,6 @@ module.exports = React.createClass({
} else this.markFieldError(fieldID, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); } else this.markFieldError(fieldID, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
break; break;
} }
case FIELD_USERNAME: {
const username = this.state.username;
if (allowEmpty && username === '') {
this.markFieldError(fieldID, true);
} else if (username == '') {
this.markFieldError(
fieldID,
false,
"RegistrationForm.ERR_USERNAME_BLANK",
);
} else {
this.markFieldError(fieldID, true);
}
break;
}
case FIELD_PASSWORD: case FIELD_PASSWORD:
if (allowEmpty && pwd1 === "") { if (allowEmpty && pwd1 === "") {
this.markFieldError(fieldID, true); this.markFieldError(fieldID, true);
@ -358,9 +357,14 @@ module.exports = React.createClass({
validateUsernameRules: withValidation({ validateUsernameRules: withValidation({
description: () => _t("Use letters, numbers, dashes and underscores only"), description: () => _t("Use letters, numbers, dashes and underscores only"),
rules: [ rules: [
{
key: "required",
test: ({ value, allowEmpty }) => allowEmpty || !!value,
invalid: () => _t("Enter username"),
},
{ {
key: "safeLocalpart", key: "safeLocalpart",
regex: SAFE_LOCALPART_REGEX, test: ({ value }) => !value || SAFE_LOCALPART_REGEX.test(value),
invalid: () => _t("Some characters not allowed"), invalid: () => _t("Some characters not allowed"),
}, },
], ],
@ -393,7 +397,6 @@ module.exports = React.createClass({
renderUsername() { renderUsername() {
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
return <Field return <Field
className={this._classForField(FIELD_USERNAME)}
id="mx_RegistrationForm_username" id="mx_RegistrationForm_username"
ref={field => this[FIELD_USERNAME] = field} ref={field => this[FIELD_USERNAME] = field}
type="text" type="text"

View file

@ -87,14 +87,15 @@ export default class Field extends React.PureComponent {
this.input.focus(); this.input.focus();
} }
validate({ focused }) { validate({ focused, allowEmpty = true }) {
if (!this.props.onValidate) { if (!this.props.onValidate) {
return; return;
} }
const { value } = this.input; const value = this.input ? this.input.value : null;
const { valid, feedback } = this.props.onValidate({ const { valid, feedback } = this.props.onValidate({
value, value,
focused, focused,
allowEmpty,
}); });
this.setState({ this.setState({
valid, valid,

View file

@ -26,7 +26,7 @@ import classNames from 'classnames';
* An array of rules describing how to check to input value. Each rule in an object * An array of rules describing how to check to input value. Each rule in an object
* and may have the following properties: * and may have the following properties:
* - `key`: A unique ID for the rule. Required. * - `key`: A unique ID for the rule. Required.
* - `regex`: A regex used to determine the rule's current validity. Required. * - `test`: A function used to determine the rule's current validity. Required.
* - `valid`: Function returning text to show when the rule is valid. Only shown if set. * - `valid`: Function returning text to show when the rule is valid. Only shown if set.
* - `invalid`: Function returning text to show when the rule is invalid. Only shown if set. * - `invalid`: Function returning text to show when the rule is invalid. Only shown if set.
* @returns {Function} * @returns {Function}
@ -34,9 +34,9 @@ import classNames from 'classnames';
* the overall validity and a feedback UI that can be rendered for more detail. * the overall validity and a feedback UI that can be rendered for more detail.
*/ */
export default function withValidation({ description, rules }) { export default function withValidation({ description, rules }) {
return function onValidate({ value, focused }) { return function onValidate({ value, focused, allowEmpty = true }) {
// TODO: Re-run only after ~200ms of inactivity // TODO: Re-run only after ~200ms of inactivity
if (!value) { if (!value && allowEmpty) {
return { return {
valid: null, valid: null,
feedback: null, feedback: null,
@ -47,10 +47,10 @@ export default function withValidation({ description, rules }) {
let valid = true; let valid = true;
if (rules && rules.length) { if (rules && rules.length) {
for (const rule of rules) { for (const rule of rules) {
if (!rule.key || !rule.regex) { if (!rule.key || !rule.test) {
continue; continue;
} }
const ruleValid = rule.regex.test(value); const ruleValid = rule.test({ value, allowEmpty });
valid = valid && ruleValid; valid = valid && ruleValid;
if (ruleValid && rule.valid) { if (ruleValid && rule.valid) {
// If the rule's result is valid and has text to show for // If the rule's result is valid and has text to show for

View file

@ -1324,6 +1324,7 @@
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
"Use letters, numbers, dashes and underscores only": "Use letters, numbers, dashes and underscores only", "Use letters, numbers, dashes and underscores only": "Use letters, numbers, dashes and underscores only",
"Some characters not allowed": "Some characters not allowed", "Some characters not allowed": "Some characters not allowed",
"Enter username": "Enter username",
"Create your Matrix account": "Create your Matrix account", "Create your Matrix account": "Create your Matrix account",
"Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s",
"Email (optional)": "Email (optional)", "Email (optional)": "Email (optional)",
@ -1524,7 +1525,6 @@
"This doesn't look like a valid phone number.": "This doesn't look like a valid phone number.", "This doesn't look like a valid phone number.": "This doesn't look like a valid phone number.",
"An email address is required to register on this homeserver.": "An email address is required to register on this homeserver.", "An email address is required to register on this homeserver.": "An email address is required to register on this homeserver.",
"A phone number is required to register on this homeserver.": "A phone number is required to register on this homeserver.", "A phone number is required to register on this homeserver.": "A phone number is required to register on this homeserver.",
"You need to enter a username.": "You need to enter a username.",
"An unknown error occurred.": "An unknown error occurred.", "An unknown error occurred.": "An unknown error occurred.",
"Create your account": "Create your account", "Create your account": "Create your account",
"Commands": "Commands", "Commands": "Commands",