Skip to content

Commit

Permalink
Update validation checks
Browse files Browse the repository at this point in the history
  • Loading branch information
Etheryte committed Sep 26, 2024
1 parent f1d676a commit 84737d6
Show file tree
Hide file tree
Showing 5 changed files with 31 additions and 40 deletions.
50 changes: 25 additions & 25 deletions web/html/src/components/input/InputBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ export type InputBaseProps<ValueType = string> = {
};

type State = {
isValid: boolean;
isTouched: boolean;

/**
Expand All @@ -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<number, ValidationResult>;
};

export class InputBase<ValueType = string> extends React.Component<InputBaseProps<ValueType>, State> {
Expand All @@ -120,10 +119,9 @@ export class InputBase<ValueType = string> extends React.Component<InputBaseProp
constructor(props: InputBaseProps<ValueType>) {
super(props);
this.state = {
isValid: true,
isTouched: false,
formErrors: undefined,
validationErrors: [],
validationErrors: new Map(),
};
}

Expand Down Expand Up @@ -187,11 +185,11 @@ export class InputBase<ValueType = string> extends React.Component<InputBaseProp

componentWillUnmount() {
if (Object.keys(this.context).length > 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);
}
}
}
Expand All @@ -202,10 +200,6 @@ export class InputBase<ValueType = string> extends React.Component<InputBaseProp
});
};

isValid() {
return this.state.isValid;
}

isEmptyValue(input: unknown) {
if (typeof input === "string") {
return input.trim() === "";
Expand Down Expand Up @@ -238,7 +232,6 @@ export class InputBase<ValueType = string> extends React.Component<InputBaseProp
*/
validate = _debounce(
async <InferredValueType extends unknown = ValueType>(value: InferredValueType): Promise<void> => {
// 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
Expand All @@ -257,14 +250,14 @@ export class InputBase<ValueType = string> extends React.Component<InputBaseProp
return;
}

// BUG: We don't handle race conditions here, it's a bug, but it will get fixed for free this once we swap this code out for Formik
const result = await validator(value);
// console.log(validator, result);
this.setState((state) => {
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 {
Expand All @@ -275,13 +268,21 @@ export class InputBase<ValueType = string> extends React.Component<InputBaseProp
})
);

if (this.context.validateForm != null) {
this.context.validateForm();
}
this.context.validateForm?.();
},
this.props.debounceValidate ?? 0
);

isValid = () => {
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 });
};
Expand Down Expand Up @@ -326,14 +327,13 @@ export class InputBase<ValueType = string> extends React.Component<InputBaseProp
}

render() {
const hasFormError = !!this.state.formErrors?.length;
const hasError = hasFormError || (this.state.isTouched && !this.state.isValid);

const hints: React.ReactNode[] = [];
this.pushHint(hints, this.props.hint);
this.state.formErrors?.forEach((error) => this.pushHint(hints, error));
this.state.validationErrors.forEach((error) => this.pushHint(hints, error));

const hasError = this.state.isTouched && !this.isValid();

return (
<FormGroup isError={hasError} key={`${this.props.name}-group`} className={this.props.className}>
{this.props.label && (
Expand Down
9 changes: 0 additions & 9 deletions web/html/src/components/input/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,6 @@ type SyncOrAsync<T> = T | Promise<T>;
export type ValidationResult = OneOrMany<Exclude<React.ReactNode, boolean> | undefined>;
export type Validator = (...args: any[]) => SyncOrAsync<ValidationResult>;

// 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 =>
Expand Down Expand Up @@ -68,7 +60,6 @@ const isInt =
};

export const Validate = {
all,
matches,
minLength,
maxLength,
Expand Down
2 changes: 1 addition & 1 deletion web/html/src/components/input/validation/async.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default () => {
};

return (
<Form model={model} onChange={(newModel) => setModel(newModel)}>
<Form model={model} onChange={setModel}>
<p>Async validation with debounce:</p>
<Text name="foo" validate={[asyncValidator]} debounceValidate={500} />
</Form>
Expand Down
8 changes: 4 additions & 4 deletions web/html/src/components/input/validation/helpers.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ export default () => {
const [model, setModel] = useState({ foo: "Foo", bar: "3", tea: "Hi" });

return (
<Form model={model} onChange={(newModel) => setModel(newModel)}>
<Form model={model} onChange={setModel}>
<p>There are numerous validation helpers:</p>
<Text name="foo" validate={[Validate.all([Validate.matches(/^F/), Validate.minLength(3)])]} />
<Text name="bar" validate={[Validate.isInt()]} />
<Text name="tea" validate={[Validate.matches(/[a-z]/, "Must include a lowercase letter")]} />
<Text name="foo" validate={[Validate.matches(/^F/), Validate.minLength(3)]} />
<Text name="bar" validate={Validate.isInt()} />
<Text name="tea" validate={Validate.matches(/[a-z]/, "Must include a lowercase letter")} />
</Form>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default () => {
};

return (
<Form model={model} onChange={(newModel) => setModel(newModel)}>
<Form model={model} onChange={setModel}>
<p>You can return multiple validation errors:</p>
<Text name="foo" validate={[validator]} />
</Form>
Expand Down

0 comments on commit 84737d6

Please sign in to comment.