refactor: Use class for validateWithIndicator (@fehmer) (#7151)

In preparation for the ElementWithUtils refactoring
This commit is contained in:
Christian Fehmer 2025-11-26 23:29:57 +01:00 committed by GitHub
parent 227a522024
commit 71b5d22159
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 89 additions and 97 deletions

View file

@ -142,80 +142,75 @@ export type ValidationOptions<T> = (T extends string
callback?: (result: ValidationResult) => void;
};
export type ValidatedHtmlInputElement = HTMLInputElement & {
getValidationResult: () => ValidationResult;
setValue: (val: string | null) => void;
triggerValidation: () => void;
};
/**
* adds an 'InputIndicator` to the given `inputElement` and updates its status depending on the given validation
* @param inputElement
* @param options
*/
export function validateWithIndicator<T>(
inputElement: HTMLInputElement,
options: ValidationOptions<T>
): ValidatedHtmlInputElement {
//use indicator
const indicator = new InputIndicator(inputElement, {
success: {
icon: "fa-check",
level: 1,
},
failed: {
icon: "fa-times",
level: -1,
},
warning: {
icon: "fa-exclamation-triangle",
level: 1,
},
checking: {
icon: "fa-circle-notch",
spinIcon: true,
level: 0,
},
});
let currentStatus: ValidationResult = {
export class ValidatedHtmlInputElement<T = string> {
public native: HTMLInputElement;
private indicator: InputIndicator;
private currentStatus: ValidationResult = {
status: "checking",
};
const callback = (result: ValidationResult): void => {
currentStatus = result;
if (result.status === "failed" || result.status === "warning") {
indicator.show(result.status, result.errorMessage);
} else {
indicator.show(result.status);
}
options.callback?.(result);
};
const handler = createInputEventHandler(
callback,
options,
"inputValueConvert" in options ? options.inputValueConvert : undefined
);
constructor(inputElement: HTMLInputElement, options: ValidationOptions<T>) {
this.native = inputElement;
inputElement.addEventListener("input", handler);
this.indicator = new InputIndicator(inputElement, {
success: {
icon: "fa-check",
level: 1,
},
failed: {
icon: "fa-times",
level: -1,
},
warning: {
icon: "fa-exclamation-triangle",
level: 1,
},
checking: {
icon: "fa-circle-notch",
spinIcon: true,
level: 0,
},
});
const result = inputElement as ValidatedHtmlInputElement;
result.getValidationResult = () => {
return currentStatus;
};
result.setValue = (val: string | null) => {
inputElement.value = val ?? "";
const callback = (result: ValidationResult): void => {
this.currentStatus = result;
if (result.status === "failed" || result.status === "warning") {
this.indicator.show(result.status, result.errorMessage);
} else {
this.indicator.show(result.status);
}
options.callback?.(result);
};
const handler = createInputEventHandler(
callback,
options,
"inputValueConvert" in options ? options.inputValueConvert : undefined
);
inputElement.addEventListener("input", handler);
}
getValidationResult(): ValidationResult {
return this.currentStatus;
}
setValue(val: string | null): this {
this.native.value = val ?? "";
if (val === null) {
indicator.hide();
currentStatus = { status: "checking" };
this.indicator.hide();
this.currentStatus = { status: "checking" };
} else {
inputElement.dispatchEvent(new Event("input"));
this.native.dispatchEvent(new Event("input"));
}
};
result.triggerValidation = () => {
inputElement.dispatchEvent(new Event("input"));
};
return result;
return this;
}
getValue(): string {
return this.native.value;
}
triggerValidation(): void {
this.native.dispatchEvent(new Event("input"));
}
}
export type ConfigInputOptions<K extends ConfigKey, T = ConfigType[K]> = {
@ -260,7 +255,7 @@ export function handleConfigInput<T extends ConfigKey>({
if (validation !== undefined) {
const schema = ConfigSchema.shape[configName] as ZodType;
validateWithIndicator(input, {
new ValidatedHtmlInputElement(input, {
schema: validation.schema ? schema : undefined,
//@ts-expect-error this is fine
isValid: validation.isValid,

View file

@ -1,5 +1,5 @@
import { getfpsLimit, fpsLimitSchema, setfpsLimit } from "../../anim";
import { validateWithIndicator } from "../input-validation";
import { ValidatedHtmlInputElement } from "../input-validation";
import * as Notifications from "../notifications";
const section = document.querySelector(
@ -10,7 +10,7 @@ const button = section.querySelector(
"button[data-fpsLimit='native']"
) as HTMLButtonElement;
const input = validateWithIndicator(
const input = new ValidatedHtmlInputElement(
section.querySelector('input[type="number"]') as HTMLInputElement,
{
schema: fpsLimitSchema,
@ -24,7 +24,7 @@ export function update(): void {
input.setValue(null);
button.classList.add("active");
} else {
input.value = fpsLimit.toString();
input.setValue(fpsLimit.toString());
button.classList.remove("active");
}
}
@ -38,7 +38,7 @@ function save(value: number): void {
function saveFromInput(): void {
if (input.getValidationResult().status !== "success") return;
const val = parseInt(input.value, 10);
const val = parseInt(input.getValue(), 10);
save(val);
}
@ -47,10 +47,10 @@ button.addEventListener("click", () => {
update();
});
input.addEventListener("keypress", (e) => {
input.native.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
saveFromInput();
}
});
input.addEventListener("focusout", (e) => saveFromInput());
input.native.addEventListener("focusout", (e) => saveFromInput());

View file

@ -21,10 +21,7 @@ import {
} from "@monkeytype/schemas/configs";
import { getDefaultConfig } from "../constants/default-config";
import { SnapshotPreset } from "../constants/default-snapshot";
import {
ValidatedHtmlInputElement,
validateWithIndicator,
} from "../elements/input-validation";
import { ValidatedHtmlInputElement } from "../elements/input-validation";
const state = {
presetType: "full" as PresetType,
@ -50,7 +47,7 @@ export function show(action: string, id?: string, name?: string): void {
$("#editPresetModal .modal .text").addClass("hidden");
addCheckBoxes();
if (!presetNameEl) {
presetNameEl = validateWithIndicator(
presetNameEl = new ValidatedHtmlInputElement(
document.querySelector(
"#editPresetModal .modal input"
) as HTMLInputElement,
@ -64,7 +61,7 @@ export function show(action: string, id?: string, name?: string): void {
$("#editPresetModal .modal .popupTitle").html("Add new preset");
$("#editPresetModal .modal .submit").html(`add`);
presetNameEl?.setValue(null);
presetNameEl?.parentElement?.classList.remove("hidden");
presetNameEl?.native.parentElement?.classList.remove("hidden");
$("#editPresetModal .modal input").removeClass("hidden");
$(
"#editPresetModal .modal label.changePresetToCurrentCheckbox"
@ -79,7 +76,7 @@ export function show(action: string, id?: string, name?: string): void {
$("#editPresetModal .modal .popupTitle").html("Edit preset");
$("#editPresetModal .modal .submit").html(`save`);
presetNameEl?.setValue(name);
presetNameEl?.parentElement?.classList.remove("hidden");
presetNameEl?.native.parentElement?.classList.remove("hidden");
$("#editPresetModal .modal input").removeClass("hidden");
$(
@ -108,7 +105,7 @@ export function show(action: string, id?: string, name?: string): void {
$("#editPresetModal .modal .inputs").addClass("hidden");
$("#editPresetModal .modal .presetType").addClass("hidden");
$("#editPresetModal .modal .presetNameTitle").addClass("hidden");
presetNameEl?.parentElement?.classList.add("hidden");
presetNameEl?.native.parentElement?.classList.add("hidden");
}
updateUI();
},

View file

@ -14,7 +14,7 @@ import * as Loader from "../elements/loader";
import { subscribe as subscribeToSignUpEvent } from "../observables/google-sign-up-event";
import AnimatedModal from "../utils/animated-modal";
import { resetIgnoreAuthCallback } from "../firebase";
import { validateWithIndicator } from "../elements/input-validation";
import { ValidatedHtmlInputElement } from "../elements/input-validation";
import { UserNameSchema } from "@monkeytype/schemas/users";
import { remoteValidation } from "../utils/remote-validation";
@ -153,7 +153,7 @@ function disableInput(): void {
nameInputEl.disabled = true;
}
validateWithIndicator(nameInputEl, {
new ValidatedHtmlInputElement(nameInputEl, {
schema: UserNameSchema,
isValid: remoteValidation(
async (name) => Ape.users.getNameAvailability({ params: { name } }),

View file

@ -2,7 +2,7 @@ import * as CustomText from "../test/custom-text";
import * as Notifications from "../elements/notifications";
import * as CustomTextState from "../states/custom-text-name";
import AnimatedModal, { ShowOptions } from "../utils/animated-modal";
import { validateWithIndicator } from "../elements/input-validation";
import { ValidatedHtmlInputElement } from "../elements/input-validation";
import { z } from "zod";
type IncomingData = {
@ -17,7 +17,7 @@ const state: State = {
textToSave: [],
};
const validatedInput = validateWithIndicator(
const validatedInput = new ValidatedHtmlInputElement(
$("#saveCustomTextModal .textName")[0] as HTMLInputElement,
{
debounceDelay: 500,

View file

@ -7,7 +7,7 @@ import {
UserEmailSchema,
UserNameSchema,
} from "@monkeytype/schemas/users";
import { validateWithIndicator } from "../elements/input-validation";
import { ValidatedHtmlInputElement } from "../elements/input-validation";
import { isDevEnvironment } from "../utils/misc";
import { z } from "zod";
import { remoteValidation } from "../utils/remote-validation";
@ -72,7 +72,7 @@ export function getSignupData(): SignupData | false {
const nameInputEl = document.querySelector(
".page.pageLogin .register.side input.usernameInput"
) as HTMLInputElement;
validateWithIndicator(nameInputEl, {
new ValidatedHtmlInputElement(nameInputEl, {
schema: UserNameSchema,
isValid: remoteValidation(
async (name) => Ape.users.getNameAvailability({ params: { name } }),
@ -90,7 +90,7 @@ let disposableEmailModule: typeof import("disposable-email-domains-js") | null =
null;
let moduleLoadAttempted = false;
const emailInputEl = validateWithIndicator(
const emailInputEl = new ValidatedHtmlInputElement(
document.querySelector(
".page.pageLogin .register.side input.emailInput"
) as HTMLInputElement,
@ -143,7 +143,7 @@ const emailInputEl = validateWithIndicator(
}
);
emailInputEl.addEventListener("focus", async () => {
emailInputEl.native.addEventListener("focus", async () => {
if (!moduleLoadAttempted) {
moduleLoadAttempted = true;
try {
@ -157,9 +157,9 @@ emailInputEl.addEventListener("focus", async () => {
const emailVerifyInputEl = document.querySelector(
".page.pageLogin .register.side input.verifyEmailInput"
) as HTMLInputElement;
validateWithIndicator(emailVerifyInputEl, {
new ValidatedHtmlInputElement(emailVerifyInputEl, {
isValid: async (emailVerify: string) => {
return emailInputEl.value === emailVerify
return emailInputEl.getValue() === emailVerify
? true
: "verify email not matching email";
},
@ -168,13 +168,13 @@ validateWithIndicator(emailVerifyInputEl, {
registerForm.email =
emailInputEl.getValidationResult().status === "success" &&
result.status === "success"
? emailInputEl.value
? emailInputEl.getValue()
: undefined;
updateSignupButton();
},
});
const passwordInputEl = validateWithIndicator(
const passwordInputEl = new ValidatedHtmlInputElement(
document.querySelector(
".page.pageLogin .register.side .passwordInput"
) as HTMLInputElement,
@ -192,9 +192,9 @@ const passwordInputEl = validateWithIndicator(
const passwordVerifyInputEl = document.querySelector(
".page.pageLogin .register.side .verifyPasswordInput"
) as HTMLInputElement;
validateWithIndicator(passwordVerifyInputEl, {
new ValidatedHtmlInputElement(passwordVerifyInputEl, {
isValid: async (passwordVerify: string) => {
return passwordInputEl.value === passwordVerify
return passwordInputEl.getValue() === passwordVerify
? true
: "verify password not matching password";
},
@ -203,7 +203,7 @@ validateWithIndicator(passwordVerifyInputEl, {
registerForm.password =
passwordInputEl.getValidationResult().status === "success" &&
result.status === "success"
? passwordInputEl.value
? passwordInputEl.getValue()
: undefined;
updateSignupButton();
},

View file

@ -6,10 +6,10 @@ import * as Notifications from "../elements/notifications";
import * as ConnectionState from "../states/connection";
import {
IsValidResponse,
ValidatedHtmlInputElement,
Validation,
ValidationOptions,
ValidationResult,
validateWithIndicator as withValidation,
} from "../elements/input-validation";
type CommonInput<TType, TValue> = {
@ -351,7 +351,7 @@ export class SimpleModal {
debounceDelay: input.validation.debounceDelay,
};
withValidation(element, options);
new ValidatedHtmlInputElement(element, options);
}
});