Track per-field validity with new-style validation

This updates the registration form to include the new-style validation state
when deciding whether the entire form is valid overall.

In addition, this tweaks the validation helper to take functions instead of
strings for translated text. This allows the validation helper to be create once
per component instead of once every render, which improves performance.
This commit is contained in:
J. Ryan Stinnett 2019-04-17 11:39:11 +01:00
parent 37e09b5569
commit 62a01e7a37
2 changed files with 64 additions and 36 deletions

View file

@ -68,7 +68,9 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
// Field error codes by field ID // Field error codes by field ID
// TODO: Remove `fieldErrors` once converted to new-style validation
fieldErrors: {}, fieldErrors: {},
fieldValid: {},
// The ISO2 country code selected in the phone number entry // The ISO2 country code selected in the phone number entry
phoneCountry: this.props.defaultPhoneCountry, phoneCountry: this.props.defaultPhoneCountry,
username: "", username: "",
@ -140,12 +142,19 @@ module.exports = React.createClass({
* @returns {boolean} true if all fields were valid last time they were validated. * @returns {boolean} true if all fields were valid last time they were validated.
*/ */
allFieldsValid: function() { allFieldsValid: function() {
const keys = Object.keys(this.state.fieldErrors); // TODO: Remove `fieldErrors` here when all fields converted
let keys = Object.keys(this.state.fieldErrors);
for (let i = 0; i < keys.length; ++i) { for (let i = 0; i < keys.length; ++i) {
if (this.state.fieldErrors[keys[i]]) { if (this.state.fieldErrors[keys[i]]) {
return false; return false;
} }
} }
keys = Object.keys(this.state.fieldValid);
for (let i = 0; i < keys.length; ++i) {
if (!this.state.fieldValid[keys[i]]) {
return false;
}
}
return true; return true;
}, },
@ -161,57 +170,57 @@ module.exports = React.createClass({
const email = this.state.email; const email = this.state.email;
const emailValid = email === '' || Email.looksValid(email); const emailValid = email === '' || Email.looksValid(email);
if (this._authStepIsRequired('m.login.email.identity') && (!emailValid || email === '')) { if (this._authStepIsRequired('m.login.email.identity') && (!emailValid || email === '')) {
this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_EMAIL"); this.markFieldError(fieldID, false, "RegistrationForm.ERR_MISSING_EMAIL");
} else this.markFieldValid(fieldID, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); } else this.markFieldError(fieldID, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
break; break;
} }
case FIELD_PHONE_NUMBER: { case FIELD_PHONE_NUMBER: {
const phoneNumber = this.state.phoneNumber; const phoneNumber = this.state.phoneNumber;
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber); const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
if (this._authStepIsRequired('m.login.msisdn') && (!phoneNumberValid || phoneNumber === '')) { if (this._authStepIsRequired('m.login.msisdn') && (!phoneNumberValid || phoneNumber === '')) {
this.markFieldValid(fieldID, false, "RegistrationForm.ERR_MISSING_PHONE_NUMBER"); this.markFieldError(fieldID, false, "RegistrationForm.ERR_MISSING_PHONE_NUMBER");
} else this.markFieldValid(fieldID, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); } else this.markFieldError(fieldID, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
break; break;
} }
case FIELD_USERNAME: { case FIELD_USERNAME: {
const username = this.state.username; const username = this.state.username;
if (allowEmpty && username === '') { if (allowEmpty && username === '') {
this.markFieldValid(fieldID, true); this.markFieldError(fieldID, true);
} else if (username == '') { } else if (username == '') {
this.markFieldValid( this.markFieldError(
fieldID, fieldID,
false, false,
"RegistrationForm.ERR_USERNAME_BLANK", "RegistrationForm.ERR_USERNAME_BLANK",
); );
} else { } else {
this.markFieldValid(fieldID, true); this.markFieldError(fieldID, true);
} }
break; break;
} }
case FIELD_PASSWORD: case FIELD_PASSWORD:
if (allowEmpty && pwd1 === "") { if (allowEmpty && pwd1 === "") {
this.markFieldValid(fieldID, true); this.markFieldError(fieldID, true);
} else if (pwd1 == '') { } else if (pwd1 == '') {
this.markFieldValid( this.markFieldError(
fieldID, fieldID,
false, false,
"RegistrationForm.ERR_PASSWORD_MISSING", "RegistrationForm.ERR_PASSWORD_MISSING",
); );
} else if (pwd1.length < this.props.minPasswordLength) { } else if (pwd1.length < this.props.minPasswordLength) {
this.markFieldValid( this.markFieldError(
fieldID, fieldID,
false, false,
"RegistrationForm.ERR_PASSWORD_LENGTH", "RegistrationForm.ERR_PASSWORD_LENGTH",
); );
} else { } else {
this.markFieldValid(fieldID, true); this.markFieldError(fieldID, true);
} }
break; break;
case FIELD_PASSWORD_CONFIRM: case FIELD_PASSWORD_CONFIRM:
if (allowEmpty && pwd2 === "") { if (allowEmpty && pwd2 === "") {
this.markFieldValid(fieldID, true); this.markFieldError(fieldID, true);
} else { } else {
this.markFieldValid( this.markFieldError(
fieldID, pwd1 == pwd2, fieldID, pwd1 == pwd2,
"RegistrationForm.ERR_PASSWORD_MISMATCH", "RegistrationForm.ERR_PASSWORD_MISMATCH",
); );
@ -220,7 +229,8 @@ module.exports = React.createClass({
} }
}, },
markFieldValid: function(fieldID, valid, errorCode) { markFieldError: function(fieldID, valid, errorCode) {
// TODO: Remove this function once all fields converted to new-style validation.
const { fieldErrors } = this.state; const { fieldErrors } = this.state;
if (valid) { if (valid) {
fieldErrors[fieldID] = null; fieldErrors[fieldID] = null;
@ -235,6 +245,14 @@ module.exports = React.createClass({
this.props.onValidationChange(fieldErrors); this.props.onValidationChange(fieldErrors);
}, },
markFieldValid: function(fieldID, valid) {
const { fieldValid } = this.state;
fieldValid[fieldID] = valid;
this.setState({
fieldValid,
});
},
_classForField: function(fieldID, ...baseClasses) { _classForField: function(fieldID, ...baseClasses) {
let cls = baseClasses.join(' '); let cls = baseClasses.join(' ');
// TODO: Remove this from fields as they are converted to new-style validation. // TODO: Remove this from fields as they are converted to new-style validation.
@ -298,6 +316,23 @@ module.exports = React.createClass({
}); });
}, },
onUsernameValidate(fieldState) {
const result = this.validateUsernameRules(fieldState);
this.markFieldValid(FIELD_USERNAME, result.valid);
return result;
},
validateUsernameRules: withValidation({
description: () => _t("Use letters, numbers, dashes and underscores only"),
rules: [
{
key: "safeLocalpart",
regex: SAFE_LOCALPART_REGEX,
invalid: () => _t("Some characters not allowed"),
},
],
}),
/** /**
* A step is required if all flows include that step. * A step is required if all flows include that step.
* *
@ -324,18 +359,6 @@ module.exports = React.createClass({
renderUsername() { renderUsername() {
const Field = sdk.getComponent('elements.Field'); const Field = sdk.getComponent('elements.Field');
const onValidate = withValidation({
description: _t("Use letters, numbers, dashes and underscores only"),
rules: [
{
key: "safeLocalpart",
regex: SAFE_LOCALPART_REGEX,
invalid: _t("Some characters not allowed"),
},
],
});
return <Field return <Field
className={this._classForField(FIELD_USERNAME)} className={this._classForField(FIELD_USERNAME)}
id="mx_RegistrationForm_username" id="mx_RegistrationForm_username"
@ -345,7 +368,7 @@ module.exports = React.createClass({
defaultValue={this.props.defaultUsername} defaultValue={this.props.defaultUsername}
value={this.state.username} value={this.state.username}
onChange={this.onUsernameChange} onChange={this.onUsernameChange}
onValidate={onValidate} onValidate={this.onUsernameValidate}
/>; />;
}, },

View file

@ -19,16 +19,16 @@ import classNames from 'classnames';
/** /**
* Creates a validation function from a set of rules describing what to validate. * Creates a validation function from a set of rules describing what to validate.
* *
* @param {String} description * @param {Function} description
* Summary of the kind of value that will meet the validation rules. Shown at * Function that returns a string summary of the kind of value that will
* the top of the validation feedback. * meet the validation rules. Shown at the top of the validation feedback.
* @param {Object} rules * @param {Object} rules
* 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. * - `regex`: A regex used to determine the rule's current validity. Required.
* - `valid`: 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`: 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}
* A validation function that takes in the current input value and returns * A validation function that takes in the current input value and returns
* 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.
@ -58,7 +58,7 @@ export default function withValidation({ description, rules }) {
results.push({ results.push({
key: rule.key, key: rule.key,
valid: true, valid: true,
text: rule.valid, text: rule.valid(),
}); });
} else if (!ruleValid && rule.invalid) { } else if (!ruleValid && rule.invalid) {
// If the rule's result is invalid and has text to show for // If the rule's result is invalid and has text to show for
@ -66,7 +66,7 @@ export default function withValidation({ description, rules }) {
results.push({ results.push({
key: rule.key, key: rule.key,
valid: false, valid: false,
text: rule.invalid, text: rule.invalid(),
}); });
} }
} }
@ -96,8 +96,13 @@ export default function withValidation({ description, rules }) {
</ul>; </ul>;
} }
let summary;
if (description) {
summary = <div className="mx_Validation_description">{description()}</div>;
}
const feedback = <div className="mx_Validation"> const feedback = <div className="mx_Validation">
<div className="mx_Validation_description">{description}</div> {summary}
{details} {details}
</div>; </div>;