Skip to content

Commit 71b5d22

Browse files
authored
refactor: Use class for validateWithIndicator (@fehmer) (monkeytypegame#7151)
In preparation for the ElementWithUtils refactoring
1 parent 227a522 commit 71b5d22

File tree

7 files changed

+89
-97
lines changed

7 files changed

+89
-97
lines changed

frontend/src/ts/elements/input-validation.ts

Lines changed: 61 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -142,80 +142,75 @@ export type ValidationOptions<T> = (T extends string
142142
callback?: (result: ValidationResult) => void;
143143
};
144144

145-
export type ValidatedHtmlInputElement = HTMLInputElement & {
146-
getValidationResult: () => ValidationResult;
147-
setValue: (val: string | null) => void;
148-
triggerValidation: () => void;
149-
};
150-
/**
151-
* adds an 'InputIndicator` to the given `inputElement` and updates its status depending on the given validation
152-
* @param inputElement
153-
* @param options
154-
*/
155-
export function validateWithIndicator<T>(
156-
inputElement: HTMLInputElement,
157-
options: ValidationOptions<T>
158-
): ValidatedHtmlInputElement {
159-
//use indicator
160-
const indicator = new InputIndicator(inputElement, {
161-
success: {
162-
icon: "fa-check",
163-
level: 1,
164-
},
165-
failed: {
166-
icon: "fa-times",
167-
level: -1,
168-
},
169-
warning: {
170-
icon: "fa-exclamation-triangle",
171-
level: 1,
172-
},
173-
checking: {
174-
icon: "fa-circle-notch",
175-
spinIcon: true,
176-
level: 0,
177-
},
178-
});
179-
180-
let currentStatus: ValidationResult = {
145+
export class ValidatedHtmlInputElement<T = string> {
146+
public native: HTMLInputElement;
147+
private indicator: InputIndicator;
148+
private currentStatus: ValidationResult = {
181149
status: "checking",
182150
};
183-
const callback = (result: ValidationResult): void => {
184-
currentStatus = result;
185-
if (result.status === "failed" || result.status === "warning") {
186-
indicator.show(result.status, result.errorMessage);
187-
} else {
188-
indicator.show(result.status);
189-
}
190-
options.callback?.(result);
191-
};
192151

193-
const handler = createInputEventHandler(
194-
callback,
195-
options,
196-
"inputValueConvert" in options ? options.inputValueConvert : undefined
197-
);
152+
constructor(inputElement: HTMLInputElement, options: ValidationOptions<T>) {
153+
this.native = inputElement;
198154

199-
inputElement.addEventListener("input", handler);
155+
this.indicator = new InputIndicator(inputElement, {
156+
success: {
157+
icon: "fa-check",
158+
level: 1,
159+
},
160+
failed: {
161+
icon: "fa-times",
162+
level: -1,
163+
},
164+
warning: {
165+
icon: "fa-exclamation-triangle",
166+
level: 1,
167+
},
168+
checking: {
169+
icon: "fa-circle-notch",
170+
spinIcon: true,
171+
level: 0,
172+
},
173+
});
200174

201-
const result = inputElement as ValidatedHtmlInputElement;
202-
result.getValidationResult = () => {
203-
return currentStatus;
204-
};
205-
result.setValue = (val: string | null) => {
206-
inputElement.value = val ?? "";
175+
const callback = (result: ValidationResult): void => {
176+
this.currentStatus = result;
177+
if (result.status === "failed" || result.status === "warning") {
178+
this.indicator.show(result.status, result.errorMessage);
179+
} else {
180+
this.indicator.show(result.status);
181+
}
182+
options.callback?.(result);
183+
};
184+
185+
const handler = createInputEventHandler(
186+
callback,
187+
options,
188+
"inputValueConvert" in options ? options.inputValueConvert : undefined
189+
);
190+
191+
inputElement.addEventListener("input", handler);
192+
}
193+
194+
getValidationResult(): ValidationResult {
195+
return this.currentStatus;
196+
}
197+
setValue(val: string | null): this {
198+
this.native.value = val ?? "";
207199
if (val === null) {
208-
indicator.hide();
209-
currentStatus = { status: "checking" };
200+
this.indicator.hide();
201+
this.currentStatus = { status: "checking" };
210202
} else {
211-
inputElement.dispatchEvent(new Event("input"));
203+
this.native.dispatchEvent(new Event("input"));
212204
}
213-
};
214-
result.triggerValidation = () => {
215-
inputElement.dispatchEvent(new Event("input"));
216-
};
217205

218-
return result;
206+
return this;
207+
}
208+
getValue(): string {
209+
return this.native.value;
210+
}
211+
triggerValidation(): void {
212+
this.native.dispatchEvent(new Event("input"));
213+
}
219214
}
220215

221216
export type ConfigInputOptions<K extends ConfigKey, T = ConfigType[K]> = {
@@ -260,7 +255,7 @@ export function handleConfigInput<T extends ConfigKey>({
260255
if (validation !== undefined) {
261256
const schema = ConfigSchema.shape[configName] as ZodType;
262257

263-
validateWithIndicator(input, {
258+
new ValidatedHtmlInputElement(input, {
264259
schema: validation.schema ? schema : undefined,
265260
//@ts-expect-error this is fine
266261
isValid: validation.isValid,

frontend/src/ts/elements/settings/fps-limit-section.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getfpsLimit, fpsLimitSchema, setfpsLimit } from "../../anim";
2-
import { validateWithIndicator } from "../input-validation";
2+
import { ValidatedHtmlInputElement } from "../input-validation";
33
import * as Notifications from "../notifications";
44

55
const section = document.querySelector(
@@ -10,7 +10,7 @@ const button = section.querySelector(
1010
"button[data-fpsLimit='native']"
1111
) as HTMLButtonElement;
1212

13-
const input = validateWithIndicator(
13+
const input = new ValidatedHtmlInputElement(
1414
section.querySelector('input[type="number"]') as HTMLInputElement,
1515
{
1616
schema: fpsLimitSchema,
@@ -24,7 +24,7 @@ export function update(): void {
2424
input.setValue(null);
2525
button.classList.add("active");
2626
} else {
27-
input.value = fpsLimit.toString();
27+
input.setValue(fpsLimit.toString());
2828
button.classList.remove("active");
2929
}
3030
}
@@ -38,7 +38,7 @@ function save(value: number): void {
3838

3939
function saveFromInput(): void {
4040
if (input.getValidationResult().status !== "success") return;
41-
const val = parseInt(input.value, 10);
41+
const val = parseInt(input.getValue(), 10);
4242
save(val);
4343
}
4444

@@ -47,10 +47,10 @@ button.addEventListener("click", () => {
4747
update();
4848
});
4949

50-
input.addEventListener("keypress", (e) => {
50+
input.native.addEventListener("keypress", (e) => {
5151
if (e.key === "Enter") {
5252
saveFromInput();
5353
}
5454
});
5555

56-
input.addEventListener("focusout", (e) => saveFromInput());
56+
input.native.addEventListener("focusout", (e) => saveFromInput());

frontend/src/ts/modals/edit-preset.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ import {
2121
} from "@monkeytype/schemas/configs";
2222
import { getDefaultConfig } from "../constants/default-config";
2323
import { SnapshotPreset } from "../constants/default-snapshot";
24-
import {
25-
ValidatedHtmlInputElement,
26-
validateWithIndicator,
27-
} from "../elements/input-validation";
24+
import { ValidatedHtmlInputElement } from "../elements/input-validation";
2825

2926
const state = {
3027
presetType: "full" as PresetType,
@@ -50,7 +47,7 @@ export function show(action: string, id?: string, name?: string): void {
5047
$("#editPresetModal .modal .text").addClass("hidden");
5148
addCheckBoxes();
5249
if (!presetNameEl) {
53-
presetNameEl = validateWithIndicator(
50+
presetNameEl = new ValidatedHtmlInputElement(
5451
document.querySelector(
5552
"#editPresetModal .modal input"
5653
) as HTMLInputElement,
@@ -64,7 +61,7 @@ export function show(action: string, id?: string, name?: string): void {
6461
$("#editPresetModal .modal .popupTitle").html("Add new preset");
6562
$("#editPresetModal .modal .submit").html(`add`);
6663
presetNameEl?.setValue(null);
67-
presetNameEl?.parentElement?.classList.remove("hidden");
64+
presetNameEl?.native.parentElement?.classList.remove("hidden");
6865
$("#editPresetModal .modal input").removeClass("hidden");
6966
$(
7067
"#editPresetModal .modal label.changePresetToCurrentCheckbox"
@@ -79,7 +76,7 @@ export function show(action: string, id?: string, name?: string): void {
7976
$("#editPresetModal .modal .popupTitle").html("Edit preset");
8077
$("#editPresetModal .modal .submit").html(`save`);
8178
presetNameEl?.setValue(name);
82-
presetNameEl?.parentElement?.classList.remove("hidden");
79+
presetNameEl?.native.parentElement?.classList.remove("hidden");
8380

8481
$("#editPresetModal .modal input").removeClass("hidden");
8582
$(
@@ -108,7 +105,7 @@ export function show(action: string, id?: string, name?: string): void {
108105
$("#editPresetModal .modal .inputs").addClass("hidden");
109106
$("#editPresetModal .modal .presetType").addClass("hidden");
110107
$("#editPresetModal .modal .presetNameTitle").addClass("hidden");
111-
presetNameEl?.parentElement?.classList.add("hidden");
108+
presetNameEl?.native.parentElement?.classList.add("hidden");
112109
}
113110
updateUI();
114111
},

frontend/src/ts/modals/google-sign-up.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import * as Loader from "../elements/loader";
1414
import { subscribe as subscribeToSignUpEvent } from "../observables/google-sign-up-event";
1515
import AnimatedModal from "../utils/animated-modal";
1616
import { resetIgnoreAuthCallback } from "../firebase";
17-
import { validateWithIndicator } from "../elements/input-validation";
17+
import { ValidatedHtmlInputElement } from "../elements/input-validation";
1818
import { UserNameSchema } from "@monkeytype/schemas/users";
1919
import { remoteValidation } from "../utils/remote-validation";
2020

@@ -153,7 +153,7 @@ function disableInput(): void {
153153
nameInputEl.disabled = true;
154154
}
155155

156-
validateWithIndicator(nameInputEl, {
156+
new ValidatedHtmlInputElement(nameInputEl, {
157157
schema: UserNameSchema,
158158
isValid: remoteValidation(
159159
async (name) => Ape.users.getNameAvailability({ params: { name } }),

frontend/src/ts/modals/save-custom-text.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as CustomText from "../test/custom-text";
22
import * as Notifications from "../elements/notifications";
33
import * as CustomTextState from "../states/custom-text-name";
44
import AnimatedModal, { ShowOptions } from "../utils/animated-modal";
5-
import { validateWithIndicator } from "../elements/input-validation";
5+
import { ValidatedHtmlInputElement } from "../elements/input-validation";
66
import { z } from "zod";
77

88
type IncomingData = {
@@ -17,7 +17,7 @@ const state: State = {
1717
textToSave: [],
1818
};
1919

20-
const validatedInput = validateWithIndicator(
20+
const validatedInput = new ValidatedHtmlInputElement(
2121
$("#saveCustomTextModal .textName")[0] as HTMLInputElement,
2222
{
2323
debounceDelay: 500,

frontend/src/ts/pages/login.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
UserEmailSchema,
88
UserNameSchema,
99
} from "@monkeytype/schemas/users";
10-
import { validateWithIndicator } from "../elements/input-validation";
10+
import { ValidatedHtmlInputElement } from "../elements/input-validation";
1111
import { isDevEnvironment } from "../utils/misc";
1212
import { z } from "zod";
1313
import { remoteValidation } from "../utils/remote-validation";
@@ -72,7 +72,7 @@ export function getSignupData(): SignupData | false {
7272
const nameInputEl = document.querySelector(
7373
".page.pageLogin .register.side input.usernameInput"
7474
) as HTMLInputElement;
75-
validateWithIndicator(nameInputEl, {
75+
new ValidatedHtmlInputElement(nameInputEl, {
7676
schema: UserNameSchema,
7777
isValid: remoteValidation(
7878
async (name) => Ape.users.getNameAvailability({ params: { name } }),
@@ -90,7 +90,7 @@ let disposableEmailModule: typeof import("disposable-email-domains-js") | null =
9090
null;
9191
let moduleLoadAttempted = false;
9292

93-
const emailInputEl = validateWithIndicator(
93+
const emailInputEl = new ValidatedHtmlInputElement(
9494
document.querySelector(
9595
".page.pageLogin .register.side input.emailInput"
9696
) as HTMLInputElement,
@@ -143,7 +143,7 @@ const emailInputEl = validateWithIndicator(
143143
}
144144
);
145145

146-
emailInputEl.addEventListener("focus", async () => {
146+
emailInputEl.native.addEventListener("focus", async () => {
147147
if (!moduleLoadAttempted) {
148148
moduleLoadAttempted = true;
149149
try {
@@ -157,9 +157,9 @@ emailInputEl.addEventListener("focus", async () => {
157157
const emailVerifyInputEl = document.querySelector(
158158
".page.pageLogin .register.side input.verifyEmailInput"
159159
) as HTMLInputElement;
160-
validateWithIndicator(emailVerifyInputEl, {
160+
new ValidatedHtmlInputElement(emailVerifyInputEl, {
161161
isValid: async (emailVerify: string) => {
162-
return emailInputEl.value === emailVerify
162+
return emailInputEl.getValue() === emailVerify
163163
? true
164164
: "verify email not matching email";
165165
},
@@ -168,13 +168,13 @@ validateWithIndicator(emailVerifyInputEl, {
168168
registerForm.email =
169169
emailInputEl.getValidationResult().status === "success" &&
170170
result.status === "success"
171-
? emailInputEl.value
171+
? emailInputEl.getValue()
172172
: undefined;
173173
updateSignupButton();
174174
},
175175
});
176176

177-
const passwordInputEl = validateWithIndicator(
177+
const passwordInputEl = new ValidatedHtmlInputElement(
178178
document.querySelector(
179179
".page.pageLogin .register.side .passwordInput"
180180
) as HTMLInputElement,
@@ -192,9 +192,9 @@ const passwordInputEl = validateWithIndicator(
192192
const passwordVerifyInputEl = document.querySelector(
193193
".page.pageLogin .register.side .verifyPasswordInput"
194194
) as HTMLInputElement;
195-
validateWithIndicator(passwordVerifyInputEl, {
195+
new ValidatedHtmlInputElement(passwordVerifyInputEl, {
196196
isValid: async (passwordVerify: string) => {
197-
return passwordInputEl.value === passwordVerify
197+
return passwordInputEl.getValue() === passwordVerify
198198
? true
199199
: "verify password not matching password";
200200
},
@@ -203,7 +203,7 @@ validateWithIndicator(passwordVerifyInputEl, {
203203
registerForm.password =
204204
passwordInputEl.getValidationResult().status === "success" &&
205205
result.status === "success"
206-
? passwordInputEl.value
206+
? passwordInputEl.getValue()
207207
: undefined;
208208
updateSignupButton();
209209
},

frontend/src/ts/utils/simple-modal.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import * as Notifications from "../elements/notifications";
66
import * as ConnectionState from "../states/connection";
77
import {
88
IsValidResponse,
9+
ValidatedHtmlInputElement,
910
Validation,
1011
ValidationOptions,
1112
ValidationResult,
12-
validateWithIndicator as withValidation,
1313
} from "../elements/input-validation";
1414

1515
type CommonInput<TType, TValue> = {
@@ -351,7 +351,7 @@ export class SimpleModal {
351351
debounceDelay: input.validation.debounceDelay,
352352
};
353353

354-
withValidation(element, options);
354+
new ValidatedHtmlInputElement(element, options);
355355
}
356356
});
357357

0 commit comments

Comments
 (0)