From 0266b2a559952d0ff767b251c2921704a6aa1abe Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sat, 8 Mar 2025 21:04:32 +0100 Subject: [PATCH 1/2] Added form preloading, and autofocus on the first input element Also adds the preloading functionality to login & register pages --- src/snek/static/generic-form.js | 188 +++++++++++++++++++------------ src/snek/templates/login.html | 2 +- src/snek/templates/register.html | 2 +- src/snek/view/login.py | 2 +- src/snek/view/register.py | 2 +- 5 files changed, 118 insertions(+), 78 deletions(-) diff --git a/src/snek/static/generic-form.js b/src/snek/static/generic-form.js index 11647dc..73e55cb 100644 --- a/src/snek/static/generic-form.js +++ b/src/snek/static/generic-form.js @@ -98,58 +98,58 @@ class GenericField extends HTMLElement { } button { - width: 50%; - padding: 10px; - background-color: #f05a28; - border: none; - float: right; - margin-top: 10px; - margin-left: 10px; - margin-right: 10px; - border-radius: 5px; - color: white; - font-size: 1em; - font-weight: bold; - cursor: pointer; - transition: background-color 0.3s; - clear: both; + width: 50%; + padding: 10px; + background-color: #f05a28; + border: none; + float: right; + margin-top: 10px; + margin-left: 10px; + margin-right: 10px; + border-radius: 5px; + color: white; + font-size: 1em; + font-weight: bold; + cursor: pointer; + transition: background-color 0.3s; + clear: both; } button:hover { - background-color: #e04924; + background-color: #e04924; } a { - color: #f05a28; - text-decoration: none; - display: block; - margin-top: 15px; - font-size: 0.9em; - transition: color 0.3s; + color: #f05a28; + text-decoration: none; + display: block; + margin-top: 15px; + font-size: 0.9em; + transition: color 0.3s; } a:hover { - color: #e04924; + color: #e04924; } .valid { - border: 1px solid green; - color: green; - font-size: 0.9em; - margin-top: 5px; + border: 1px solid green; + color: green; + font-size: 0.9em; + margin-top: 5px; } .error { border: 3px solid red; - color: #d8000c; - font-size: 0.9em; - margin-top: 5px; + color: #d8000c; + font-size: 0.9em; + margin-top: 5px; } @media (max-width: 500px) { - input { - width: 90%; - } + input { + width: 90%; + } } `; this.container.appendChild(this.styleElement); @@ -165,7 +165,13 @@ class GenericField extends HTMLElement { this[name] = value; } + focus(options) { + this.inputElement?.focus(options); + } + updateAttributes() { + const inputUpdate = this.inputElement != null; + if (this.inputElement == null && this.field) { this.inputElement = document.createElement(this.field.tag); if (this.field.tag === 'button' && this.field.value === "submit") { @@ -218,7 +224,9 @@ class GenericField extends HTMLElement { } this.inputElement.setAttribute("tabindex", this.field.index); this.inputElement.classList.add(this.field.name); - this.value = this.field.value; + if (this.field.value != null || !inputUpdate) { + this.value = this.field.value; + } let place_holder = this.field.place_holder ?? null; if (this.field.required && place_holder) { @@ -281,64 +289,96 @@ class GenericForm extends HTMLElement { } connectedCallback() { + const preloadedForm = this.getAttribute('preloaded-structure'); + if (preloadedForm) { + try { + const form = JSON.parse(preloadedForm); + this.constructForm(form) + } catch (error) { + console.error(error, preloadedForm); + } + } const url = this.getAttribute('url'); if (url) { const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get"); if (!url.startsWith("/")) { fullUrl.searchParams.set('url', url); } - this.loadForm(fullUrl.toString()); + this.loadForm(fullUrl.toString()) } else { this.container.textContent = "No URL provided!"; } } + async constructForm(formPayload) { + try { + this.form = formPayload; + + let fields = Object.values(this.form.fields); + + let hasAutoFocus = Object.keys(this.fields).length !== 0; + + fields.sort((a, b) => a.index - b.index); + fields.forEach(field => { + const updatingField = field.name in this.fields + + this.fields[field.name] ??= document.createElement('generic-field'); + + const fieldElement = this.fields[field.name]; + + fieldElement.setAttribute("form", this); + fieldElement.setAttribute("field", field); + + fieldElement.updateAttributes(); + + if (!updatingField) { + this.container.appendChild(fieldElement); + + if (!hasAutoFocus && field.tag === "input") { + fieldElement.focus(); + hasAutoFocus = true; + } + + fieldElement.addEventListener("change", (e) => { + this.form.fields[e.detail.name].value = e.detail.value; + }); + + fieldElement.addEventListener("click", async (e) => { + if (e.detail.type === "button" && e.detail.value === "submit") { + const isValid = await this.validate(); + if (isValid) { + const saveResult = await this.submit(); + if (saveResult.redirect_url) { + window.location.pathname = saveResult.redirect_url; + } + } + } + }); + + fieldElement.addEventListener("submit", async (e) => { + const isValid = await this.validate(); + if (isValid) { + const saveResult = await this.submit(); + if (saveResult.redirect_url) { + window.location.pathname = saveResult.redirect_url; + } + } + }) + } + }); + } catch (error) { + this.container.textContent = `Error: ${error.message}`; + } + } + async loadForm(url) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`); } - this.form = await response.json(); - - let fields = Object.values(this.form.fields); - - fields.sort((a, b) => a.index - b.index); - fields.forEach(field => { - const fieldElement = document.createElement('generic-field'); - this.fields[field.name] = fieldElement; - fieldElement.setAttribute("form", this); - fieldElement.setAttribute("field", field); - this.container.appendChild(fieldElement); - fieldElement.updateAttributes(); - - fieldElement.addEventListener("change", (e) => { - this.form.fields[e.detail.name].value = e.detail.value; - }); - - fieldElement.addEventListener("click", async (e) => { - if (e.detail.type === "button" && e.detail.value === "submit") { - const isValid = await this.validate(); - if (isValid) { - const saveResult = await this.submit(); - if (saveResult.redirect_url) { - window.location.pathname = saveResult.redirect_url; - } - } - } - }); - - fieldElement.addEventListener("submit", async (e) => { - const isValid = await this.validate(); - if (isValid) { - const saveResult = await this.submit(); - if (saveResult.redirect_url) { - window.location.pathname = saveResult.redirect_url; - } - } - }) - }); + await this.constructForm(await response.json()); } catch (error) { this.container.textContent = `Error: ${error.message}`; } diff --git a/src/snek/templates/login.html b/src/snek/templates/login.html index ed81224..d91920c 100644 --- a/src/snek/templates/login.html +++ b/src/snek/templates/login.html @@ -11,6 +11,6 @@ {% block main %}