From 71b5d2215961888b4cc7f5a3d2f7d8c8a17eec57 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Wed, 26 Nov 2025 23:29:57 +0100 Subject: [PATCH] refactor: Use class for validateWithIndicator (@fehmer) (#7151) In preparation for the ElementWithUtils refactoring --- frontend/src/ts/elements/input-validation.ts | 127 +++++++++--------- .../ts/elements/settings/fps-limit-section.ts | 12 +- frontend/src/ts/modals/edit-preset.ts | 13 +- frontend/src/ts/modals/google-sign-up.ts | 4 +- frontend/src/ts/modals/save-custom-text.ts | 4 +- frontend/src/ts/pages/login.ts | 22 +-- frontend/src/ts/utils/simple-modal.ts | 4 +- 7 files changed, 89 insertions(+), 97 deletions(-) diff --git a/frontend/src/ts/elements/input-validation.ts b/frontend/src/ts/elements/input-validation.ts index 5dce09c44..277bcd8d3 100644 --- a/frontend/src/ts/elements/input-validation.ts +++ b/frontend/src/ts/elements/input-validation.ts @@ -142,80 +142,75 @@ export type ValidationOptions = (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( - inputElement: HTMLInputElement, - options: ValidationOptions -): 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 { + 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) { + 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 = { @@ -260,7 +255,7 @@ export function handleConfigInput({ 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, diff --git a/frontend/src/ts/elements/settings/fps-limit-section.ts b/frontend/src/ts/elements/settings/fps-limit-section.ts index fb1b99412..1c0e52303 100644 --- a/frontend/src/ts/elements/settings/fps-limit-section.ts +++ b/frontend/src/ts/elements/settings/fps-limit-section.ts @@ -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()); diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index 5b235f12d..3efa27fd4 100644 --- a/frontend/src/ts/modals/edit-preset.ts +++ b/frontend/src/ts/modals/edit-preset.ts @@ -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(); }, diff --git a/frontend/src/ts/modals/google-sign-up.ts b/frontend/src/ts/modals/google-sign-up.ts index fc452a05e..5724456d3 100644 --- a/frontend/src/ts/modals/google-sign-up.ts +++ b/frontend/src/ts/modals/google-sign-up.ts @@ -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 } }), diff --git a/frontend/src/ts/modals/save-custom-text.ts b/frontend/src/ts/modals/save-custom-text.ts index 6bf9cd8d6..cde6e32dc 100644 --- a/frontend/src/ts/modals/save-custom-text.ts +++ b/frontend/src/ts/modals/save-custom-text.ts @@ -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, diff --git a/frontend/src/ts/pages/login.ts b/frontend/src/ts/pages/login.ts index 7111ec3fa..8380fc158 100644 --- a/frontend/src/ts/pages/login.ts +++ b/frontend/src/ts/pages/login.ts @@ -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(); }, diff --git a/frontend/src/ts/utils/simple-modal.ts b/frontend/src/ts/utils/simple-modal.ts index a311fee46..599d821b8 100644 --- a/frontend/src/ts/utils/simple-modal.ts +++ b/frontend/src/ts/utils/simple-modal.ts @@ -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 = { @@ -351,7 +351,7 @@ export class SimpleModal { debounceDelay: input.validation.debounceDelay, }; - withValidation(element, options); + new ValidatedHtmlInputElement(element, options); } });