From ff246483b342da7f039ffbd4e713d0c10154405d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=20Jurmanovi=C4=87?= Date: Sat, 5 Jun 2021 22:06:08 +0200 Subject: [PATCH] custom app form --- .../app-dropdown/AppDropdownElement.ts | 74 +++++++++++ src/components/app-form/AppFormElement.ts | 123 ++++++++++++++++++ src/components/index.ts | 1 + .../input-field/InputFieldElement.ts | 60 +++++++-- .../services/router-service/RouterService.ts | 10 +- src/core/utils/index.ts | 2 + src/core/utils/query-deco.ts | 15 +++ src/core/utils/querys-deco.ts | 18 +++ src/pages/login-page/LoginPageElement.ts | 20 ++- 9 files changed, 301 insertions(+), 22 deletions(-) create mode 100644 src/components/app-dropdown/AppDropdownElement.ts create mode 100644 src/components/app-form/AppFormElement.ts create mode 100644 src/core/utils/query-deco.ts create mode 100644 src/core/utils/querys-deco.ts diff --git a/src/components/app-dropdown/AppDropdownElement.ts b/src/components/app-dropdown/AppDropdownElement.ts new file mode 100644 index 0000000..3bc04d1 --- /dev/null +++ b/src/components/app-dropdown/AppDropdownElement.ts @@ -0,0 +1,74 @@ +import { attr, controller, target } from "@github/catalyst"; +import { firstUpper } from "core/utils"; +import { html, TemplateResult } from "@github/jtml"; +import { RouterService } from "core/services"; +import randomId from "core/utils/random-id"; +import validator from "validator"; +import { validatorErrors } from "core/constants"; +import { BaseComponentElement } from "common/"; + +@controller +class AppDropdownElement extends BaseComponentElement { + @attr name: string; + @attr type: string; + @attr label: string; + @attr rules: string; + @target main: HTMLElement; + @target inp: HTMLElement; + error: string; + randId: string; + constructor() { + super(); + } + + public elementConnected = (): void => { + this.randId = `${name}${randomId()}`; + this.update(); + }; + + get valid(): boolean { + return !!this.error; + } + + get required(): boolean { + return this.rules.includes("required"); + } + + validate(): boolean { + let _return = true; + const rules = this.rules?.split("|").filter((a) => a); + const value = (this.inp as HTMLInputElement)?.value; + rules + .slice() + .reverse() + .forEach((rule) => { + let valid = true; + if (rule == "required") { + if (value === "") valid = false; + } else { + if (validator.hasOwnProperty(rule)) { + valid = validator?.[rule]?.(value); + } + } + if (!valid) { + const error = validatorErrors[rule]?.replaceAll( + "{- name}", + firstUpper(this.name?.toString()) + ); + _return = false; + this.error = error; + } + }); + if (_return) { + this.error = null; + } + this.update(); + return _return; + } + + render = (): TemplateResult => { + return html``; + }; +} + +export type { AppDropdownElement }; diff --git a/src/components/app-form/AppFormElement.ts b/src/components/app-form/AppFormElement.ts new file mode 100644 index 0000000..6fc3eea --- /dev/null +++ b/src/components/app-form/AppFormElement.ts @@ -0,0 +1,123 @@ +import { attr, controller, target } from "@github/catalyst"; +import { html, TemplateResult, unsafeHTML } from "@github/jtml"; +import { BaseComponentElement } from "common/"; +import { InputFieldElement } from "components/input-field/InputFieldElement"; +import { querys } from "core/utils"; + +@controller +class AppFormElement extends BaseComponentElement { + @target formElement: HTMLElement; + @target innerSlot: HTMLElement; + @querys inputField: NodeListOf; + @attr custom: string; + slotted: any; + isValid: boolean = false; + error: string; + constructor() { + super(); + } + + public inputChange = (e) => { + this.validate(); + this.update(); + }; + + public keyUp = (e) => { + if (e.keyCode === 13) { + this.onSubmit(e); + } + }; + + public onSubmit = (e) => { + e.preventDefault(); + if (!this.valid) { + return; + } + const actionString = this.custom; + if (actionString) { + const methodSep = actionString.lastIndexOf("#"); + const tag = actionString.slice(0, methodSep); + const method = actionString.slice(methodSep + 1); + + const element = this.appMain.querySelector(tag); + if (element) { + element?.[method]?.(e); + } + } + return false; + }; + + public validate = () => { + this.isValid = true; + this.inputField?.forEach((input) => { + if (input?.error) { + this.isValid = false; + } + }); + }; + + public setError = (error) => { + this.error = error; + this.update(); + }; + + public goBack = (e) => { + e.preventDefault(); + this.routerService?.goBack(); + }; + + get valid() { + let _valid = 0; + this.inputField?.forEach((input) => { + if (input.required && (input.inp as HTMLInputElement).value) { + _valid++; + } + }); + return _valid == this.inputField?.length; + } + + elementConnected = (): void => { + const _template = document.createElement("template"); + const _slot = this.innerHTML; + _template.innerHTML = _slot; + this.innerHTML = null; + this.update(); + + this.formElement.replaceChild(_template.content, this.innerSlot); + }; + + render = (): TemplateResult => { + const renderSubmit = (valid: boolean) => { + if (!valid) { + return html` `; + } + return html` `; + }; + const renderError = (error: string) => { + if (error) { + return html`${error}`; + } + return html``; + }; + const renderCancel = () => { + return html``; + }; + + return html`
+ + ${renderError(this.error)}${renderSubmit( + this.isValid + )}${renderCancel()} +
`; + }; +} + +export type { AppFormElement }; diff --git a/src/components/index.ts b/src/components/index.ts index ef901af..fb25dd1 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -8,6 +8,7 @@ export * from "./app-menu/AppMenuElement"; export * from "./input-field/InputFieldElement"; export * from "./app-loader/AppLoaderElement"; export * from "./circle-loader/CircleLoaderElement"; +export * from "./app-form/AppFormElement"; // LAST export * from "./app-main/AppMainElement"; diff --git a/src/components/input-field/InputFieldElement.ts b/src/components/input-field/InputFieldElement.ts index feff3dc..593b3c0 100644 --- a/src/components/input-field/InputFieldElement.ts +++ b/src/components/input-field/InputFieldElement.ts @@ -1,11 +1,12 @@ import { attr, controller, target } from "@github/catalyst"; -import { firstUpper } from "core/utils"; +import { closest, firstUpper } from "core/utils"; import { html, TemplateResult } from "@github/jtml"; import { RouterService } from "core/services"; import randomId from "core/utils/random-id"; import validator from "validator"; import { validatorErrors } from "core/constants"; import { BaseComponentElement } from "common/"; +import { AppFormElement } from "components/app-form/AppFormElement"; @controller class InputFieldElement extends BaseComponentElement { @@ -15,7 +16,9 @@ class InputFieldElement extends BaseComponentElement { @attr rules: string; @target main: HTMLElement; @target inp: HTMLElement; + @closest appForm: AppFormElement; error: string; + displayError: boolean; randId: string; constructor() { super(); @@ -34,7 +37,7 @@ class InputFieldElement extends BaseComponentElement { return this.rules.includes("required"); } - validate(): boolean { + validate = (): boolean => { let _return = true; const rules = this.rules?.split("|").filter((a) => a); const value = (this.inp as HTMLInputElement)?.value; @@ -62,18 +65,55 @@ class InputFieldElement extends BaseComponentElement { if (_return) { this.error = null; } - this.update(); return _return; - } + }; + + validateDisplay = () => { + if (!this.validate()) { + this.displayError = true; + } else { + this.displayError = false; + } + this.update(); + }; + + inputChange = (e) => { + this.validate(); + this.appForm?.inputChange(e); + }; render = (): TemplateResult => { + const renderMessage = (label: string) => { + if (this.label) { + return html``; + } + return html``; + }; + + const renderError = (displayError: boolean, error: string) => { + if (displayError) { + return html`${error}`; + } + return html``; + }; + + const renderInput = (type) => { + return html` `; + }; + return html`
- ${this.label && - html``} - - ${this.error && html`${this.error}`} + ${renderMessage(this.label)} ${renderInput(this.type)} + ${renderError(this.displayError, this.error)}
`; }; } diff --git a/src/core/services/router-service/RouterService.ts b/src/core/services/router-service/RouterService.ts index 54dde44..999643c 100644 --- a/src/core/services/router-service/RouterService.ts +++ b/src/core/services/router-service/RouterService.ts @@ -194,7 +194,7 @@ class RouterService { public goBack = (): void => { if (!Array.isArray(this.historyStack)) this.historyStack = []; const lenHistory = this.historyStack.length; - if (lenHistory > 1) { + if (this.canGoBack) { const nextRoute = this.historyStack[lenHistory - 2]; const url = new URL(window.location.toString()); url.pathname = nextRoute.path; @@ -204,6 +204,14 @@ class RouterService { this.update(); }; + public get canGoBack(): boolean { + const lenHistory = this.historyStack.length; + if (lenHistory > 2) { + return true; + } + return false; + } + public init = (): void => { window.addEventListener("popstate", () => { this.historyStack.pop(); diff --git a/src/core/utils/index.ts b/src/core/utils/index.ts index 6ff2da0..1e9f53e 100644 --- a/src/core/utils/index.ts +++ b/src/core/utils/index.ts @@ -4,3 +4,5 @@ export { default as index } from "./index-deco"; export { default as closest } from "./closest-deco"; export { default as isTrue } from "./isTrue"; export { default as firstUpper } from "./first-upper"; +export { default as query } from "./query-deco"; +export { default as querys } from "./querys-deco"; diff --git a/src/core/utils/query-deco.ts b/src/core/utils/query-deco.ts new file mode 100644 index 0000000..bb6cfcf --- /dev/null +++ b/src/core/utils/query-deco.ts @@ -0,0 +1,15 @@ +import { toKebabCase } from "core/utils"; + +export default function query(proto: Object, key: string): any { + const kebab: string = toKebabCase(key); + return Object.defineProperty(proto, key, { + configurable: true, + get() { + return findQuery(this, kebab); + }, + }); +} + +function findQuery(element: HTMLElement, key: string): HTMLElement { + return element.querySelector(key); +} diff --git a/src/core/utils/querys-deco.ts b/src/core/utils/querys-deco.ts new file mode 100644 index 0000000..6380686 --- /dev/null +++ b/src/core/utils/querys-deco.ts @@ -0,0 +1,18 @@ +import { toKebabCase } from "core/utils"; + +export default function querys(proto: Object, key: string): any { + const kebab: string = toKebabCase(key); + return Object.defineProperty(proto, key, { + configurable: true, + get() { + return findQuerys(this, kebab); + }, + }); +} + +function findQuerys( + element: HTMLElement, + key: string +): NodeListOf { + return element.querySelectorAll(key); +} diff --git a/src/pages/login-page/LoginPageElement.ts b/src/pages/login-page/LoginPageElement.ts index 7f85386..383a5bd 100644 --- a/src/pages/login-page/LoginPageElement.ts +++ b/src/pages/login-page/LoginPageElement.ts @@ -1,15 +1,15 @@ -import { targets, controller } from "@github/catalyst"; +import { targets, controller, target } from "@github/catalyst"; import { html, TemplateResult } from "@github/jtml"; import { AuthService } from "services/"; -import { InputFieldElement } from "components/"; +import { AppFormElement, InputFieldElement } from "components/"; import { RouterService } from "core/services"; import { BasePageElement } from "common/"; @controller class LoginPageElement extends BasePageElement { @targets inputs: Array; + @target appForm: AppFormElement; authService: AuthService; - errorMessage: string; constructor() { super(); } @@ -63,8 +63,7 @@ class LoginPageElement extends BasePageElement { this.passwordInput.error = err?.message; this.passwordInput.update(); } else { - this.errorMessage = "Unable to log in!"; - this.update(); + this.appForm?.setError("Unable to log in!"); } } }; @@ -80,7 +79,10 @@ class LoginPageElement extends BasePageElement { render = (): TemplateResult => { return html` -
+ - ${this.errorMessage && html`
${this.errorMessage}
`} - - +