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 {
2025-03-08 21:04:32 +01:00
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 {
2025-03-08 21:04:32 +01:00
background-color: #e04924;
2025-02-03 20:45:29 +01:00
}
a {
2025-03-08 21:04:32 +01:00
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 {
2025-03-08 21:04:32 +01:00
color: #e04924;
2025-02-03 20:45:29 +01:00
}
.valid {
2025-03-08 21:04:32 +01:00
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;
2025-03-08 21:04:32 +01:00
color: #d8000c;
font-size: 0.9em;
margin-top: 5px;
2025-02-03 20:45:29 +01:00
}
@media (max-width: 500px) {
2025-03-08 21:04:32 +01:00
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 ;
}
2025-03-08 21:04:32 +01:00
focus ( options ) {
this . inputElement ? . focus ( options );
}
2025-02-03 20:45:29 +01:00
updateAttributes () {
2025-03-08 21:04:32 +01:00
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 ,
});
2025-03-08 18:09:14 +01:00
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-03-08 18:09:14 +01:00
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 );
2025-03-08 21:04:32 +01:00
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" );
2025-03-08 21:04:32 +01:00
if ( preloadedForm ) {
try {
const form = JSON . parse ( preloadedForm );
2025-05-23 00:44:18 +02:00
this . constructForm ( form );
2025-03-08 21:04:32 +01:00
} 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!" ;
}
}
2025-03-08 21:04:32 +01:00
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-03-08 21:04:32 +01:00
2025-05-23 00:44:18 +02:00
this . fields [ field . name ] ??= document . createElement ( "generic-field" );
2025-03-08 21:04:32 +01:00
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
});
2025-03-08 21:04:32 +01: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
2025-03-08 21:04:32 +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 );