Added form preloading, and autofocus on the first input element

Also adds the preloading functionality to login & register pages
This commit is contained in:
BordedDev 2025-03-08 21:04:32 +01:00 committed by BordedDev
parent 11b8f0e744
commit 0266b2a559
5 changed files with 118 additions and 78 deletions

View File

@ -98,58 +98,58 @@ class GenericField extends HTMLElement {
} }
button { button {
width: 50%; width: 50%;
padding: 10px; padding: 10px;
background-color: #f05a28; background-color: #f05a28;
border: none; border: none;
float: right; float: right;
margin-top: 10px; margin-top: 10px;
margin-left: 10px; margin-left: 10px;
margin-right: 10px; margin-right: 10px;
border-radius: 5px; border-radius: 5px;
color: white; color: white;
font-size: 1em; font-size: 1em;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s; transition: background-color 0.3s;
clear: both; clear: both;
} }
button:hover { button:hover {
background-color: #e04924; background-color: #e04924;
} }
a { a {
color: #f05a28; color: #f05a28;
text-decoration: none; text-decoration: none;
display: block; display: block;
margin-top: 15px; margin-top: 15px;
font-size: 0.9em; font-size: 0.9em;
transition: color 0.3s; transition: color 0.3s;
} }
a:hover { a:hover {
color: #e04924; color: #e04924;
} }
.valid { .valid {
border: 1px solid green; border: 1px solid green;
color: green; color: green;
font-size: 0.9em; font-size: 0.9em;
margin-top: 5px; margin-top: 5px;
} }
.error { .error {
border: 3px solid red; border: 3px solid red;
color: #d8000c; color: #d8000c;
font-size: 0.9em; font-size: 0.9em;
margin-top: 5px; margin-top: 5px;
} }
@media (max-width: 500px) { @media (max-width: 500px) {
input { input {
width: 90%; width: 90%;
} }
} }
`; `;
this.container.appendChild(this.styleElement); this.container.appendChild(this.styleElement);
@ -165,7 +165,13 @@ class GenericField extends HTMLElement {
this[name] = value; this[name] = value;
} }
focus(options) {
this.inputElement?.focus(options);
}
updateAttributes() { updateAttributes() {
const inputUpdate = this.inputElement != null;
if (this.inputElement == null && this.field) { if (this.inputElement == null && this.field) {
this.inputElement = document.createElement(this.field.tag); this.inputElement = document.createElement(this.field.tag);
if (this.field.tag === 'button' && this.field.value === "submit") { 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.setAttribute("tabindex", this.field.index);
this.inputElement.classList.add(this.field.name); 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; let place_holder = this.field.place_holder ?? null;
if (this.field.required && place_holder) { if (this.field.required && place_holder) {
@ -281,64 +289,96 @@ class GenericForm extends HTMLElement {
} }
connectedCallback() { 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'); const url = this.getAttribute('url');
if (url) { if (url) {
const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get"); const fullUrl = url.startsWith("/") ? window.location.origin + url : new URL(window.location.origin + "/http-get");
if (!url.startsWith("/")) { if (!url.startsWith("/")) {
fullUrl.searchParams.set('url', url); fullUrl.searchParams.set('url', url);
} }
this.loadForm(fullUrl.toString()); this.loadForm(fullUrl.toString())
} else { } else {
this.container.textContent = "No URL provided!"; 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) { async loadForm(url) {
try { try {
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`); 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) { } catch (error) {
this.container.textContent = `Error: ${error.message}`; this.container.textContent = `Error: ${error.message}`;
} }

View File

@ -11,6 +11,6 @@
{% block main %} {% block main %}
<div class="back-form"> <div class="back-form">
<fancy-button url="/back" text="Back" size="auto"></fancy-button> <fancy-button url="/back" text="Back" size="auto"></fancy-button>
<generic-form class="center" url="/login.json"></generic-form> <generic-form class="center" url="/login.json" preloaded-structure='{{ form|tojson|safe }}'></generic-form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -12,6 +12,6 @@
<div class="back-form"> <div class="back-form">
<fancy-button url="/back" text="Back" size="auto"></fancy-button> <fancy-button url="/back" text="Back" size="auto"></fancy-button>
<generic-form class="center" url="/register.json"></generic-form> <generic-form class="center" url="/register.json" preloaded-structure='{{ form|tojson|safe }}'></generic-form>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -18,7 +18,7 @@ class LoginView(BaseFormView):
return web.HTTPFound("/web.html") return web.HTTPFound("/web.html")
if self.request.path.endswith(".json"): if self.request.path.endswith(".json"):
return await super().get() return await super().get()
return await self.render_template("login.html") return await self.render_template("login.html", {"form": await self.form(app=self.app).to_json()})
async def submit(self, form): async def submit(self, form):
if await form.is_valid: if await form.is_valid:

View File

@ -18,7 +18,7 @@ class RegisterView(BaseFormView):
return web.HTTPFound("/web.html") return web.HTTPFound("/web.html")
if self.request.path.endswith(".json"): if self.request.path.endswith(".json"):
return await super().get() return await super().get()
return await self.render_template("register.html") return await self.render_template("register.html", {"form": await self.form(app=self.app).to_json()})
async def submit(self, form): async def submit(self, form):
result = await self.app.services.user.register( result = await self.app.services.user.register(