diff --git a/web/html/src/components/input/InputBase.tsx b/web/html/src/components/input/InputBase.tsx index 174be610433..ef46b51e28a 100644 --- a/web/html/src/components/input/InputBase.tsx +++ b/web/html/src/components/input/InputBase.tsx @@ -89,7 +89,6 @@ export type InputBaseProps = { }; type State = { - isValid: boolean; isTouched: boolean; /** @@ -98,9 +97,9 @@ type State = { formErrors?: string[]; /** - * Validation errors + * Validation errors, a `Map` from a given validator to its result */ - validationErrors: ValidationResult[]; + validationErrors: Map; }; export class InputBase extends React.Component, State> { @@ -120,10 +119,9 @@ export class InputBase extends React.Component) { super(props); this.state = { - isValid: true, isTouched: false, formErrors: undefined, - validationErrors: [], + validationErrors: new Map(), }; } @@ -187,11 +185,11 @@ export class InputBase extends React.Component 0) { - this.context.unregisterInput(this); - if (this.props.name instanceof Array) { - this.props.name.forEach((name) => this.context.setModelValue(name, undefined)); - } else { - this.context.setModelValue(this.props.name, undefined); + this.context.unregisterInput?.(this); + if (Array.isArray(this.props.name)) { + this.props.name.forEach((name) => this.context.setModelValue?.(name, undefined)); + } else if (this.props.name) { + this.context.setModelValue?.(this.props.name, undefined); } } } @@ -202,10 +200,6 @@ export class InputBase extends React.Component extends React.Component(value: InferredValueType): Promise => { - // TODO: If it's an array, automatically wrap it in `Validate.all()` const validators = Array.isArray(this.props.validate) ? this.props.validate : [this.props.validate] ?? []; // TODO: Move this into render so it's always sync and up to date instantly @@ -257,14 +250,14 @@ export class InputBase extends React.Component { - const newValidationErrors = [...state.validationErrors]; + const newValidationErrors = new Map(state.validationErrors); if (result) { - newValidationErrors[index] = result; + newValidationErrors.set(index, result); } else { - newValidationErrors[index] = undefined; + newValidationErrors.delete(index); } return { @@ -275,13 +268,21 @@ export class InputBase extends React.Component { + if (this.state.formErrors && this.state.formErrors.length > 0) { + return false; + } + if (this.state.validationErrors.size > 0) { + return false; + } + return true; + }; + setFormErrors = (formErrors?: string[]) => { this.setState({ formErrors }); }; @@ -326,14 +327,13 @@ export class InputBase extends React.Component this.pushHint(hints, error)); this.state.validationErrors.forEach((error) => this.pushHint(hints, error)); + const hasError = this.state.isTouched && !this.isValid(); + return ( {this.props.label && ( diff --git a/web/html/src/components/input/validate.ts b/web/html/src/components/input/validate.ts index c3261d7ec71..490d017d012 100644 --- a/web/html/src/components/input/validate.ts +++ b/web/html/src/components/input/validate.ts @@ -6,14 +6,6 @@ type SyncOrAsync = T | Promise; export type ValidationResult = OneOrMany | undefined>; export type Validator = (...args: any[]) => SyncOrAsync; -// TODO: This is really internal, do we need to expose this? -const all = - (validators: Validator[]): Validator => - async (value: string) => { - const result = await Promise.all(validators.map((item) => item(value))); - return result.filter((item) => item !== undefined).flat(); - }; - /** String must match `regex` */ const matches = (regex: RegExp, message?: string): Validator => @@ -68,7 +60,6 @@ const isInt = }; export const Validate = { - all, matches, minLength, maxLength, diff --git a/web/html/src/components/input/validation/async.stories.tsx b/web/html/src/components/input/validation/async.stories.tsx index 22a16f9ea5d..313fde4b034 100644 --- a/web/html/src/components/input/validation/async.stories.tsx +++ b/web/html/src/components/input/validation/async.stories.tsx @@ -16,7 +16,7 @@ export default () => { }; return ( -
setModel(newModel)}> +

Async validation with debounce:

diff --git a/web/html/src/components/input/validation/helpers.stories.tsx b/web/html/src/components/input/validation/helpers.stories.tsx index 7a611551a9d..40224d9469f 100644 --- a/web/html/src/components/input/validation/helpers.stories.tsx +++ b/web/html/src/components/input/validation/helpers.stories.tsx @@ -6,11 +6,11 @@ export default () => { const [model, setModel] = useState({ foo: "Foo", bar: "3", tea: "Hi" }); return ( -
setModel(newModel)}> +

There are numerous validation helpers:

- - - + + + ); }; diff --git a/web/html/src/components/input/validation/multiple-messages.stories.tsx b/web/html/src/components/input/validation/multiple-messages.stories.tsx index 46f8125fb40..43a2518c194 100644 --- a/web/html/src/components/input/validation/multiple-messages.stories.tsx +++ b/web/html/src/components/input/validation/multiple-messages.stories.tsx @@ -12,7 +12,7 @@ export default () => { }; return ( -
setModel(newModel)}> +

You can return multiple validation errors: