Files
snek/src/snek/static/generic-form.js
T

449 lines
12 KiB
JavaScript
Raw Normal View History

2025-02-03 20:45:29 +01:00
// Written by retoor@molodetz.nl
// This code defines two custom HTML elements, `GenericField` and `GenericForm`. The `GenericField` element represents a form field with validation and styling functionalities, and the `GenericForm` fetches and manages form data, handling field validation and submission.
// No external imports are present; all utilized functionality is native to JavaScript and web APIs.
// MIT License
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
2025-01-24 03:28:43 +01:00
class GenericField extends HTMLElement {
2025-02-03 20:45:29 +01:00
form = null;
field = null;
inputElement = null;
footerElement = null;
action = null;
container = null;
styleElement = null;
name = null;
2025-01-24 03:28:43 +01:00
2025-02-03 20:45:29 +01:00
get value() {
return this.inputElement.value;
}
2025-01-24 03:28:43 +01:00
2025-02-03 20:45:29 +01:00
get type() {
return this.field.tag;
}
2025-01-24 03:28:43 +01:00
2025-02-03 20:45:29 +01:00
set value(val) {
2025-05-23 00:44:18 +02:00
val = val ?? "";
2025-02-03 20:45:29 +01:00
this.inputElement.value = val;
this.inputElement.setAttribute("value", val);
}
2025-01-24 03:28:43 +01:00
2025-02-03 20:45:29 +01:00
setInvalid() {
this.inputElement.classList.add("error");
this.inputElement.classList.remove("valid");
}
2025-01-24 03:28:43 +01:00
2025-02-03 20:45:29 +01:00
setErrors(errors) {
const errorText = errors.length ? errors[0] : "";
this.inputElement.setAttribute("title", errorText);
}
2025-01-24 03:28:43 +01:00
2025-02-03 20:45:29 +01:00
setValid() {
this.inputElement.classList.remove("error");
this.inputElement.classList.add("valid");
}
2025-01-24 03:28:43 +01:00
2025-02-03 20:45:29 +01:00
constructor() {
super();
2025-05-23 00:44:18 +02:00
this.attachShadow({ mode: "open" });
this.container = document.createElement("div");
this.styleElement = document.createElement("style");
2025-02-03 20:45:29 +01:00
this.styleElement.innerHTML = `
2025-01-24 03:28:43 +01:00
2025-02-03 20:45:29 +01:00
h1 {
font-size: 2em;
color: #f05a28;
margin-bottom: 20px;
margin-top: 0px;
2025-01-24 03:28:43 +01:00
}
2025-02-03 20:45:29 +01:00
input {
2025-03-08 18:56:55 +01:00
width: 90%;
padding: 10px;
margin: 10px 0;
border: 1px solid #333;
border-radius: 5px;
background-color: #1a1a1a;
color: #e6e6e6;
font-size: 1em;
&:focus {
outline: 2px solid #f05a28 !important;
}
&::placeholder {
transition: opacity 0.3s;
}
&:focus::placeholder {
opacity: 0.4;
}
2025-02-03 20:45:29 +01:00
}
2025-03-08 18:56:55 +01:00
2025-02-03 20:45:29 +01:00
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;
2025-02-03 20:45:29 +01:00
}
button:hover {
background-color: #e04924;
2025-02-03 20:45:29 +01:00
}
a {
color: #f05a28;
text-decoration: none;
display: block;
margin-top: 15px;
font-size: 0.9em;
transition: color 0.3s;
2025-02-03 20:45:29 +01:00
}
a:hover {
color: #e04924;
2025-02-03 20:45:29 +01:00
}
.valid {
border: 1px solid green;
color: green;
font-size: 0.9em;
margin-top: 5px;
2025-02-03 20:45:29 +01:00
}
.error {
border: 3px solid red;
color: #d8000c;
font-size: 0.9em;
margin-top: 5px;
2025-02-03 20:45:29 +01:00
}
@media (max-width: 500px) {
input {
width: 90%;
}
2025-02-03 20:45:29 +01:00
}
`;
this.container.appendChild(this.styleElement);
this.shadowRoot.appendChild(this.container);
}
connectedCallback() {
this.updateAttributes();
}
setAttribute(name, value) {
this[name] = value;
}
focus(options) {
this.inputElement?.focus(options);
}
2025-02-03 20:45:29 +01:00
updateAttributes() {
const inputUpdate = this.inputElement != null;
2025-02-03 20:45:29 +01:00
if (this.inputElement == null && this.field) {
this.inputElement = document.createElement(this.field.tag);
2025-05-23 00:44:18 +02:00
if (this.field.tag === "button" && this.field.value === "submit") {
2025-02-03 20:45:29 +01:00
this.action = this.field.value;
}
this.inputElement.name = this.field.name;
this.name = this.inputElement.name;
const me = this;
this.inputElement.addEventListener("keyup", (e) => {
2025-05-23 00:44:18 +02:00
if (e.key === "Enter") {
const event = new CustomEvent("change", {
detail: me,
bubbles: true,
});
me.dispatchEvent(event);
2025-02-03 20:45:29 +01:00
me.dispatchEvent(new Event("submit"));
} else if (me.field.value !== e.target.value) {
2025-05-23 00:44:18 +02:00
const event = new CustomEvent("change", {
detail: me,
bubbles: true,
});
2025-02-03 20:45:29 +01:00
me.dispatchEvent(event);
2025-01-24 03:28:43 +01:00
}
2025-02-03 20:45:29 +01:00
});
this.inputElement.addEventListener("click", (e) => {
2025-05-23 00:44:18 +02:00
const event = new CustomEvent("click", { detail: me, bubbles: true });
2025-02-03 20:45:29 +01:00
me.dispatchEvent(event);
});
2025-05-23 00:44:18 +02:00
this.inputElement.addEventListener(
"blur",
(e) => {
const event = new CustomEvent("change", {
detail: me,
bubbles: true,
});
me.dispatchEvent(event);
},
true,
);
2025-02-03 20:45:29 +01:00
this.container.appendChild(this.inputElement);
2025-01-24 03:28:43 +01:00
}
2025-02-03 20:45:29 +01:00
if (!this.field) {
return;
}
2025-05-23 00:44:18 +02:00
this.inputElement.setAttribute("type", this.field.type ?? "input");
this.inputElement.setAttribute("name", this.field.name ?? "");
2025-02-03 20:45:29 +01:00
if (this.field.text != null) {
this.inputElement.innerText = this.field.text;
}
if (this.field.html != null) {
this.inputElement.innerHTML = this.field.html;
}
if (this.field.class_name) {
this.inputElement.classList.add(this.field.class_name);
}
this.inputElement.setAttribute("tabindex", this.field.index);
this.inputElement.classList.add(this.field.name);
if (this.field.value != null || !inputUpdate) {
this.value = this.field.value;
}
2025-02-03 20:45:29 +01:00
let place_holder = this.field.place_holder ?? null;
if (this.field.required && place_holder) {
place_holder = "* " + place_holder;
}
this.inputElement.setAttribute("placeholder", place_holder);
if (this.field.required) {
this.inputElement.setAttribute("required", "required");
} else {
this.inputElement.removeAttribute("required");
}
if (!this.footerElement) {
2025-05-23 00:44:18 +02:00
this.footerElement = document.createElement("div");
this.footerElement.style.clear = "both";
2025-02-03 20:45:29 +01:00
this.container.appendChild(this.footerElement);
}
}
2025-01-24 03:28:43 +01:00
}
2025-05-23 00:44:18 +02:00
customElements.define("generic-field", GenericField);
2025-01-24 03:28:43 +01:00
class GenericForm extends HTMLElement {
2025-02-03 20:45:29 +01:00
fields = {};
form = {};
2025-01-24 03:28:43 +01:00
constructor() {
2025-02-03 20:45:29 +01:00
super();
2025-05-23 00:44:18 +02:00
this.attachShadow({ mode: "open" });
2025-02-03 20:45:29 +01:00
this.styleElement = document.createElement("style");
this.styleElement.innerHTML = `
2025-01-24 03:28:43 +01:00
2025-02-03 20:45:29 +01:00
* {
margin: 0;
padding: 0;
box-sizing: border-box;
width: 90%;
2025-01-24 03:28:43 +01:00
}
2025-02-03 20:45:29 +01:00
div {
2025-04-02 14:55:18 +02:00
min-width:350px;
2025-02-03 20:45:29 +01:00
border-radius: 10px;
padding: 30px;
box-shadow: 0 0 15px rgba(0, 0, 0, 0.5);
text-align: center;
2025-01-24 03:28:43 +01:00
}
2025-02-03 20:45:29 +01:00
@media (max-width: 500px) {
width: 100%;
height: 100%;
form {
height: 100%;
width: 80%;
}
}`;
2025-01-24 03:28:43 +01:00
2025-05-23 00:44:18 +02:00
this.container = document.createElement("div");
2025-02-03 20:45:29 +01:00
this.container.appendChild(this.styleElement);
this.container.classList.add("generic-form-container");
this.shadowRoot.appendChild(this.container);
}
connectedCallback() {
2025-05-23 00:44:18 +02:00
const preloadedForm = this.getAttribute("preloaded-structure");
if (preloadedForm) {
try {
const form = JSON.parse(preloadedForm);
2025-05-23 00:44:18 +02:00
this.constructForm(form);
} catch (error) {
console.error(error, preloadedForm);
}
}
2025-05-23 00:44:18 +02:00
const url = this.getAttribute("url");
2025-02-03 20:45:29 +01:00
if (url) {
2025-05-23 00:44:18 +02:00
const fullUrl = url.startsWith("/")
? window.location.origin + url
: new URL(window.location.origin + "/http-get");
2025-02-03 20:45:29 +01:00
if (!url.startsWith("/")) {
2025-05-23 00:44:18 +02:00
fullUrl.searchParams.set("url", url);
2025-02-03 20:45:29 +01:00
}
2025-03-08 21:18:20 +01:00
this.loadForm(fullUrl.toString());
2025-02-03 20:45:29 +01:00
} 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);
2025-05-23 00:44:18 +02:00
fields.forEach((field) => {
const updatingField = field.name in this.fields;
2025-05-23 00:44:18 +02:00
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;
}
}
2025-05-23 00:44:18 +02:00
});
}
});
} catch (error) {
this.container.textContent = `Error: ${error.message}`;
}
}
2025-02-03 20:45:29 +01:00
async loadForm(url) {
try {
const response = await fetch(url);
if (!response.ok) {
2025-05-23 00:44:18 +02:00
throw new Error(
`Failed to fetch: ${response.status} ${response.statusText}`,
);
2025-02-03 20:45:29 +01:00
}
2025-01-24 21:19:03 +01:00
await this.constructForm(await response.json());
2025-02-03 20:45:29 +01:00
} catch (error) {
this.container.textContent = `Error: ${error.message}`;
2025-01-24 03:28:43 +01:00
}
}
2025-02-03 20:45:29 +01:00
async validate() {
const url = this.getAttribute("url");
let response = await fetch(url, {
2025-05-23 00:44:18 +02:00
method: "POST",
2025-02-03 20:45:29 +01:00
headers: {
2025-05-23 00:44:18 +02:00
"Content-Type": "application/json",
2025-02-03 20:45:29 +01:00
},
2025-05-23 00:44:18 +02:00
body: JSON.stringify({ action: "validate", form: this.form }),
2025-02-03 20:45:29 +01:00
});
const form = await response.json();
2025-05-23 00:44:18 +02:00
Object.values(form.fields).forEach((field) => {
2025-02-03 20:45:29 +01:00
if (!this.form.fields[field.name]) {
return;
}
this.form.fields[field.name].is_valid = field.is_valid;
if (!field.is_valid) {
this.fields[field.name].setInvalid();
this.fields[field.name].setErrors(field.errors);
} else {
this.fields[field.name].setValid();
}
this.fields[field.name].setAttribute("field", field);
this.fields[field.name].updateAttributes();
});
2025-05-23 00:44:18 +02:00
Object.values(form.fields).forEach((field) => {
2025-02-03 20:45:29 +01:00
this.fields[field.name].setErrors(field.errors);
});
2025-05-23 00:44:18 +02:00
return form["is_valid"];
2025-02-03 20:45:29 +01:00
}
async submit() {
const url = this.getAttribute("url");
const response = await fetch(url, {
2025-05-23 00:44:18 +02:00
method: "POST",
2025-02-03 20:45:29 +01:00
headers: {
2025-05-23 00:44:18 +02:00
"Content-Type": "application/json",
2025-02-03 20:45:29 +01:00
},
2025-05-23 00:44:18 +02:00
body: JSON.stringify({ action: "submit", form: this.form }),
2025-02-03 20:45:29 +01:00
});
return await response.json();
}
}
2025-05-23 00:44:18 +02:00
customElements.define("generic-form", GenericForm);