Validation improve pattern for derived data

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-09-21 14:35:35 +01:00
parent 8ec7e7c714
commit ed0e188b4f
2 changed files with 34 additions and 34 deletions

View file

@ -40,11 +40,7 @@ interface IProps {
onValidate(result: IValidationResult); onValidate(result: IValidationResult);
} }
interface IState { class PassphraseField extends PureComponent<IProps> {
complexity: zxcvbn.ZXCVBNResult;
}
class PassphraseField extends PureComponent<IProps, IState> {
static defaultProps = { static defaultProps = {
label: _td("Password"), label: _td("Password"),
labelEnterPassword: _td("Enter password"), labelEnterPassword: _td("Enter password"),
@ -52,14 +48,16 @@ class PassphraseField extends PureComponent<IProps, IState> {
labelAllowedButUnsafe: _td("Password is allowed, but unsafe"), labelAllowedButUnsafe: _td("Password is allowed, but unsafe"),
}; };
state = { complexity: null }; public readonly validate = withValidation<this, zxcvbn.ZXCVBNResult>({
description: function(complexity) {
public readonly validate = withValidation<this>({
description: function() {
const complexity = this.state.complexity;
const score = complexity ? complexity.score : 0; const score = complexity ? complexity.score : 0;
return <progress className="mx_PassphraseField_progress" max={4} value={score} />; return <progress className="mx_PassphraseField_progress" max={4} value={score} />;
}, },
deriveData: async ({ value }) => {
if (!value) return null;
const { scorePassword } = await import('../../../utils/PasswordScorer');
return scorePassword(value);
},
rules: [ rules: [
{ {
key: "required", key: "required",
@ -68,28 +66,24 @@ class PassphraseField extends PureComponent<IProps, IState> {
}, },
{ {
key: "complexity", key: "complexity",
test: async function({ value }) { test: async function({ value }, complexity) {
if (!value) { if (!value) {
return false; return false;
} }
const { scorePassword } = await import('../../../utils/PasswordScorer');
const complexity = scorePassword(value);
this.setState({ complexity });
const safe = complexity.score >= this.props.minScore; const safe = complexity.score >= this.props.minScore;
const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"]; const allowUnsafe = SdkConfig.get()["dangerously_allow_unsafe_and_insecure_passwords"];
return allowUnsafe || safe; return allowUnsafe || safe;
}, },
valid: function() { valid: function(complexity) {
// Unsafe passwords that are valid are only possible through a // Unsafe passwords that are valid are only possible through a
// configuration flag. We'll print some helper text to signal // configuration flag. We'll print some helper text to signal
// to the user that their password is allowed, but unsafe. // to the user that their password is allowed, but unsafe.
if (this.state.complexity.score >= this.props.minScore) { if (complexity.score >= this.props.minScore) {
return _t(this.props.labelStrongPassword); return _t(this.props.labelStrongPassword);
} }
return _t(this.props.labelAllowedButUnsafe); return _t(this.props.labelAllowedButUnsafe);
}, },
invalid: function() { invalid: function(complexity) {
const complexity = this.state.complexity;
if (!complexity) { if (!complexity) {
return null; return null;
} }

View file

@ -21,18 +21,19 @@ import classNames from "classnames";
type Data = Pick<IFieldState, "value" | "allowEmpty">; type Data = Pick<IFieldState, "value" | "allowEmpty">;
interface IRule<T> { interface IRule<T, D = void> {
key: string; key: string;
final?: boolean; final?: boolean;
skip?(this: T, data: Data): boolean; skip?(this: T, data: Data, derivedData: D): boolean;
test(this: T, data: Data): boolean | Promise<boolean>; test(this: T, data: Data, derivedData: D): boolean | Promise<boolean>;
valid?(this: T): string; valid?(this: T, derivedData: D): string;
invalid?(this: T): string; invalid?(this: T, derivedData: D): string;
} }
interface IArgs<T> { interface IArgs<T, D = void> {
rules: IRule<T>[]; rules: IRule<T, D>[];
description(this: T): React.ReactChild; description(this: T, derivedData: D): React.ReactChild;
deriveData?(data: Data): Promise<D>;
} }
export interface IFieldState { export interface IFieldState {
@ -53,6 +54,10 @@ export interface IValidationResult {
* @param {Function} description * @param {Function} description
* Function that returns a string summary of the kind of value that will * Function that returns a string summary of the kind of value that will
* meet the validation rules. Shown at the top of the validation feedback. * meet the validation rules. Shown at the top of the validation feedback.
* @param {Function} deriveData
* Optional function that returns a Promise to an object of generic type D.
* The result of this Promise is passed to rule methods `skip`, `test`, `valid`, and `invalid`.
* Useful for doing calculations per-value update once rather than in each of the above rule methods.
* @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:
@ -66,7 +71,7 @@ export interface IValidationResult {
* 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.
*/ */
export default function withValidation<T = undefined>({ description, rules }: IArgs<T>) { export default function withValidation<T = undefined, D = void>({ description, deriveData, rules }: IArgs<T, D>) {
return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> { return async function onValidate({ value, focused, allowEmpty = true }: IFieldState): Promise<IValidationResult> {
if (!value && allowEmpty) { if (!value && allowEmpty) {
return { return {
@ -75,6 +80,9 @@ export default function withValidation<T = undefined>({ description, rules }: IA
}; };
} }
const data = { value, allowEmpty };
const derivedData = deriveData ? await deriveData(data) : undefined;
const results = []; const results = [];
let valid = true; let valid = true;
if (rules && rules.length) { if (rules && rules.length) {
@ -87,20 +95,18 @@ export default function withValidation<T = undefined>({ description, rules }: IA
continue; continue;
} }
const data = { value, allowEmpty }; if (rule.skip && rule.skip.call(this, data, derivedData)) {
if (rule.skip && rule.skip.call(this, data)) {
continue; continue;
} }
// We're setting `this` to whichever component holds the validation // We're setting `this` to whichever component holds the validation
// function. That allows rules to access the state of the component. // function. That allows rules to access the state of the component.
const ruleValid = await rule.test.call(this, data); const ruleValid = await rule.test.call(this, data, derivedData);
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
// the valid state, show it. // the valid state, show it.
const text = rule.valid.call(this); const text = rule.valid.call(this, derivedData);
if (!text) { if (!text) {
continue; continue;
} }
@ -112,7 +118,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
} 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
// the invalid state, show it. // the invalid state, show it.
const text = rule.invalid.call(this); const text = rule.invalid.call(this, derivedData);
if (!text) { if (!text) {
continue; continue;
} }
@ -153,7 +159,7 @@ export default function withValidation<T = undefined>({ description, rules }: IA
if (description) { if (description) {
// We're setting `this` to whichever component holds the validation // We're setting `this` to whichever component holds the validation
// function. That allows rules to access the state of the component. // function. That allows rules to access the state of the component.
const content = description.call(this); const content = description.call(this, derivedData);
summary = <div className="mx_Validation_description">{content}</div>; summary = <div className="mx_Validation_description">{content}</div>;
} }