svelte-formique
Version:
Formique is a robust and elegant Web Content Accessibility Guidelines (WCAG) and Web Accessibility Initiative - Accessible Rich Internet Applications (WAI-ARIA)-compliant form-building library tailored for JavaScript enthusiasts.
1,539 lines (1,303 loc) • 115 kB
JavaScript
'use strict';
/**
* Formique Class Library
*
* This library provides an extension of the FormBuilder class, allowing for dynamic form rendering, theming,
* and dependency management. The key functionalities include:
*
* - Dynamic form rendering based on a provided schema (`formSchema`).
* - Theming support with predefined themes that can be applied to the form container.
* - Dependency management to show/hide fields based on parent field values.
* - Initialization of event listeners to handle form input changes.
* - **Dynamic dropdowns**: Automatically populate dropdown fields based on other form inputs.
* - **ARIA labels and WCAG compliance**: Generates forms with accessibility features, including ARIA labels for improved accessibility and compliance with Web Content Accessibility Guidelines (WCAG).
*
* Key Methods:
* - `constructor(formParams, formSchema, formSettings)`: Initializes the form with the provided parameters, schema, and settings.
* - `renderForm()`: Renders the form using the schema and appends it to the DOM.
* - `initDependencyGraph()`: Sets up the dependency graph for managing field visibility based on dependencies.
* - `attachInputChangeListener(parentField)`: Attaches input change listeners to parent fields for dependency management.
* - `handleParentFieldChange(parentFieldId, value)`: Handles changes in parent fields and updates dependent fields.
* - `registerObservers()`: Registers observers for dependent fields to manage their state based on parent field values.
* - `applyTheme(theme, formContainerId)`: Applies a specified theme to the form container.
* - `renderFormElement()`: Renders the form element with the necessary attributes and CSRF token if applicable.
* - `renderField(type, name, label, validate, attributes, options)`: Renders individual form fields based on type and attributes, including dynamic dropdowns and ARIA attributes.
*
* Dependencies:
* - The library depends on a DOM structure to initialize and manipulate form elements.
* - Requires a CSS stylesheet with theme definitions.- there are plans to internalise css themes within js
*
* Example Usage:
* const form = new Formique(formSchema,formParams,formSettings);
* - formParams and formSettings parameters are optional
*
* Author: Gugulethu Nyoni
* Version: 1.0.8
* License: Open-source & MIT licensed.
*/
class FormBuilder
{
renderField(type, name, label, validate, attributes, options) {
throw new Error('Method renderField must be implemented');
}
}
// Extended class for specific form rendering methods
class Formique extends FormBuilder {
constructor(formSchema, formParams = {}, formSettings = {}) {
super();
this.formSchema = formSchema;
this.formSettings = {
requiredFieldIndicator: true,
placeholders: true,
asteriskHtml: '<span aria-hidden="true" style="color: red;">*</span>',
...formSettings
};
this.divClass = 'input-block';
this.inputClass = 'form-input';
this.radioGroupClass = 'radio-group';
this.checkboxGroupClass = 'checkbox-group';
this.selectGroupClass = 'form-select';
this.submitButtonClass = 'form-submit-btn';
this.formParams = formParams;
this.formContainerId = formSettings.formContainerId || 'formique';
this.formAction = formParams.action || 'https://httpbin.org/post';
this.method= formParams.method.toUpperCase() || 'POST';
this.formMarkUp = ''; // '<form>';
this.dependencyGraph = {};
this.themes = [
"dark",
"light",
"pink",
"light",
"indigo",
"dark-blue",
"light-blue",
"dark-orange",
"green",
"purple",
"midnight-blush"
];
//document.addEventListener('DOMContentLoaded', () => {
if (this.formParams && Object.keys(this.formParams).length > 0) {
this.formMarkUp += this.renderFormElement();
//console.log("received",this.formMarkUp);
}
this.renderForm();
this.renderFormHTML();
this.initDependencyGraph();
this.registerObservers();
if (this.formSettings.theme && this.themes.includes(this.formSettings.theme)) {
let theme = this.formSettings.theme;
this.applyTheme(theme, this.formContainerId);
} else {
// Fallback to dark theme if no theme is set or invalid theme
this.applyTheme('dark', this.formContainerId);
}
document.getElementById(`${this.formParams.id}`).addEventListener('submit', function(event) {
if (this.formSettings.submitOnPage) {
event.preventDefault(); // Prevent the default form submission
this.handleOnPageFormSubmission(this.formParams.id);
//console.warn("listener fired at least>>", this.formParams.id, this.method);
}
}.bind(this)); // Bind `this` to ensure it's correct inside the event listener
//
// });
/*
this.formSettings = {
requiredFieldIndicator: true,
placeholders: true,
asteriskHtml: '<span aria-hidden="true" style="color: red;">*</span>',
...formSettings
};
*/
// CONSTRUCTOR WRAPPER FOR FORMIQUE CLASS
}
initDependencyGraph() {
this.dependencyGraph = {};
this.formSchema.forEach((field) => {
const [type, name, label, validate, attributes = {}] = field;
const fieldId = attributes.id || name;
if (attributes.dependents) {
// Initialize dependency array for the parent field
this.dependencyGraph[fieldId] = attributes.dependents.map((dependentName) => {
const dependentField = this.formSchema.find(
([, depName]) => depName === dependentName
);
if (dependentField) {
const dependentAttributes = dependentField[4] || {};
const dependentFieldId = dependentAttributes.id || dependentName; // Get dependent field ID
return {
dependent: dependentFieldId,
condition: dependentAttributes.condition || null,
};
} else {
console.warn(`Dependent field "${dependentName}" not found in schema.`);
}
});
// Add state tracking for the parent field
this.dependencyGraph[fieldId].push({ state: null });
// console.log("Graph", this.dependencyGraph[fieldId]);
// Attach the input change event listener to the parent field
this.attachInputChangeListener(fieldId);
}
// Hide dependent fields initially
if (attributes.dependents) {
attributes.dependents.forEach((dependentName) => {
const dependentField = this.formSchema.find(
([, depName]) => depName === dependentName
);
const dependentAttributes = dependentField ? dependentField[4] || {} : {};
const dependentFieldId = dependentAttributes.id || dependentName;
//alert(dependentFieldId);
const inputBlock = document.querySelector(`#${dependentFieldId}-block`);
//alert(inputBlock);
if (inputBlock) {
// alert(dependentName);
inputBlock.style.display = 'none'; // Hide dependent field by default
}
});
}
});
// console.log("Dependency Graph:", this.dependencyGraph);
}
// Attach Event Listeners
attachInputChangeListener(parentField) {
const fieldElement = document.getElementById(parentField);
//alert(parentField);
if (fieldElement) {
fieldElement.addEventListener('input', (event) => {
const value = event.target.value;
this.handleParentFieldChange(parentField, value);
});
}
}
handleParentFieldChange(parentFieldId, value) {
const dependencies = this.dependencyGraph[parentFieldId];
if (dependencies) {
// Update the state of the parent field
this.dependencyGraph[parentFieldId].forEach((dep) => {
if (dep.state !== undefined) {
dep.state = value; // Set state to the selected value
}
});
// Log the updated dependency graph for the parent field
// console.log(`Updated Dependency Graph for ${parentFieldId}:`, this.dependencyGraph[parentFieldId]);
// Notify all observers (dependent fields)
dependencies.forEach((dependency) => {
if (dependency.dependent) {
const observerId = dependency.dependent + "-block"; // Ensure we're targeting the wrapper
const inputBlock = document.getElementById(observerId); // Find the wrapper element
if (inputBlock) {
// Check if the condition for the observer is satisfied
const conditionMet = typeof dependency.condition === 'function'
? dependency.condition(value)
: value === dependency.condition;
// Debug the condition evaluation
// console.log(`Checking condition for ${observerId}: `, value, "==", dependency.condition, "Result:", conditionMet);
// Toggle visibility based on the condition
inputBlock.style.display = conditionMet ? 'block' : 'none';
// Adjust the 'required' attribute for all inputs within the block based on visibility
const inputs = inputBlock.querySelectorAll('input, select, textarea');
inputs.forEach((input) => {
if (conditionMet) {
input.required = input.getAttribute('data-original-required') === 'true'; // Restore original required state
} else {
input.setAttribute('data-original-required', input.required); // Save original required state
input.required = false; // Remove required attribute when hiding
}
});
} else {
console.warn(`Wrapper block with ID ${observerId} not found.`);
}
}
});
}
}
// Register observers for each dependent field
registerObservers() {
this.formSchema.forEach((field) => {
const [type, name, label, validate, attributes = {}] = field;
const fieldId = attributes.id || name;
if (attributes.dependents) {
attributes.dependents.forEach((dependentName) => {
// Ensure the dependency graph exists for the parent field
if (this.dependencyGraph[fieldId]) {
// Find the dependent field in the form schema
const dependentField = this.formSchema.find(
([, depName]) => depName === dependentName
);
// If the dependent field exists, register it as an observer
if (dependentField) {
const dependentFieldId = dependentField[4]?.id || dependentName;
this.dependencyGraph[fieldId].forEach((dependency) => {
if (dependency.dependent === dependentName) {
// Store the dependent as an observer for this parent field
if (!dependency.observers) {
dependency.observers = [];
}
dependency.observers.push(dependentFieldId);
}
});
}
}
});
}
});
// console.log("Observers Registered:", JSON.stringify(this.dependencyGraph,null,2));
}
applyTheme(theme, formContainerId) {
//const stylesheet = document.querySelector('link[formique-style]');
const stylesheet = document.querySelector('link[href*="formique-css"]');
if (!stylesheet) {
console.error("Stylesheet with 'formique-style' not found!");
return;
}
fetch(stylesheet.href)
.then(response => response.text())
.then(cssText => {
// Extract theme-specific CSS rules
const themeRules = cssText.match(new RegExp(`\\.${theme}-theme\\s*{([^}]*)}`, 'i'));
if (!themeRules) {
console.error(`Theme rules for ${theme} not found in the stylesheet.`);
return;
}
// Extract CSS rules for the theme
const themeCSS = themeRules[1].trim();
// Find the form container element
const formContainer = document.getElementById(formContainerId);
if (formContainer) {
// Append the theme class to the form container
formContainer.classList.add(`${theme}-theme`, 'formique');
// Create a <style> tag with the extracted theme styles
const clonedStyle = document.createElement('style');
clonedStyle.textContent = `
#${formContainerId} {
${themeCSS}
}
`;
// Insert the <style> tag above the form container
formContainer.parentNode.insertBefore(clonedStyle, formContainer);
// console.log(`Applied ${theme} theme to form container: ${formContainerId}`);
} else {
console.error(`Form container with ID ${formContainerId} not found.`);
}
})
.catch(error => {
console.error('Error loading the stylesheet:', error);
});
}
// renderFormElement method
renderFormElement() {
let formHTML = '<form';
// Ensure `this.formParams` is being passed in as the source of form attributes
const paramsToUse = this.formParams || {};
//console.log(paramsToUse);
// Dynamically add attributes if they are present in the parameters
Object.keys(paramsToUse).forEach(key => {
const value = paramsToUse[key];
if (value !== undefined && value !== null) {
// Handle boolean attributes (without values, just their presence)
if (typeof value === 'boolean') {
if (value) {
formHTML += ` ${key}`; // Simply add the key as the attribute
}
} else {
// Handle other attributes (key-value pairs)
const formattedKey = key === 'accept_charset' ? 'accept-charset' : key.replace(/_/g, '-');
formHTML += ` ${formattedKey}="${value}"`;
//console.log("HERE",formHTML);
}
}
});
// Conditionally add CSRF token if 'laravel' is true
if (paramsToUse.laravel) {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
if (csrfToken) {
formHTML += `<input type="hidden" name="_token" value="${csrfToken}">`;
}
}
// Close the <form> tag
formHTML += '>\n';
// Return the generated form HTML
return formHTML;
}
// Main renderForm method
renderForm() {
// Process each field synchronously
const formHTML = this.formSchema.map(field => {
const [type, name, label, validate, attributes = {},options] = field;
return this.renderField(type, name, label, validate, attributes, options);
}).join('');
this.formMarkUp += formHTML;
}
renderField(type, name, label, validate, attributes, options) {
const fieldRenderMap = {
'text': this.renderTextField,
'email': this.renderEmailField,
'number': this.renderNumberField,
'password': this.renderPasswordField,
'textarea': this.renderTextAreaField,
'tel': this.renderTelField,
'date': this.renderDateField,
'time': this.renderTimeField,
'datetime-local': this.renderDateTimeField,
'month': this.renderMonthField,
'week': this.renderWeekField,
'url': this.renderUrlField,
'search': this.renderSearchField,
'color': this.renderColorField,
'checkbox': this.renderCheckboxField,
'radio': this.renderRadioField,
'file': this.renderFileField,
'hidden': this.renderHiddenField,
'image': this.renderImageField,
'textarea': this.renderTextAreaField,
'singleSelect': this.renderSingleSelectField,
'multipleSelect': this.renderMultipleSelectField,
'dynamicSingleSelect': this.renderDynamicSingleSelectField,
'submit': this.renderSubmitButton,
};
const renderMethod = fieldRenderMap[type];
if (renderMethod) {
return renderMethod.call(this, type, name, label, validate, attributes, options);
} else {
console.warn(`Unsupported field type '${type}' encountered.`);
return ''; // or handle gracefully
}
}
// Method to handle on-page form submissions
// Method to handle on-page form submissions
handleOnPageFormSubmission(formId) {
const formElement = document.getElementById(formId);
console.warn("handler fired also", formId, this.method, this.formAction);
if (formElement) {
// Gather form data
const formData = new FormData(formElement);
const jsonObject = {};
formData.forEach((value, key) => {
jsonObject[key] = value;
});
// Log the data before sending
console.log("Form Data as JSON:", jsonObject);
// Submit form data using fetch to a test endpoint
fetch(this.formAction, {
method: this.method,
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(jsonObject),
})
.then(response => response.json())
.then(data => {
console.log("Success:", data);
// Handle the response data here, e.g., show a success message
// Get the form container element
const formContainer = document.getElementById(this.formContainerId);
if (formContainer) {
// Create a new div element for the success message
const successMessageDiv = document.createElement("div");
// Add custom classes for styling the success message
successMessageDiv.classList.add("success-message", "message-container");
// Set the success message text
successMessageDiv.innerHTML =
this.formSettings.successMessage ||
"Your details have been successfully submitted!";
// Replace the content of the form container with the success message div
formContainer.innerHTML = ""; // Clear existing content
formContainer.appendChild(successMessageDiv); // Append the new success message div
}
})
.catch(error => {
console.error("Error:", error);
const formContainer = document.getElementById(this.formContainerId);
if (formContainer) {
// Check if an error message div already exists and remove it
let existingErrorDiv = formContainer.querySelector(".error-message");
if (existingErrorDiv) {
existingErrorDiv.remove();
}
// Create a new div element for the error message
const errorMessageDiv = document.createElement("div");
// Add custom classes for styling the error message
errorMessageDiv.classList.add("error-message", "message-container");
// Set the error message text
let err =
this.formSettings.errorMessage ||
"An error occurred while submitting the form. Please try again.";
err = `${err}<br/>Details: ${error.message}`;
errorMessageDiv.innerHTML = err;
// Append the new error message div to the form container
formContainer.appendChild(errorMessageDiv);
}
});
}
}
// text field rendering
renderTextField(type, name, label, validate, attributes) {
const textInputValidationAttributes = [
'required',
'minlength',
'maxlength',
'pattern',
];
// Construct validation attributes
let validationAttrs = '';
if (validate) {
Object.entries(validate).forEach(([key, value]) => {
if (textInputValidationAttributes.includes(key)) {
if (typeof value === 'boolean' && value) {
validationAttrs += ` ${key}\n`;
} else {
switch (key) {
case 'pattern':
case 'minlength':
case 'maxlength':
validationAttrs += ` ${key}="${value}"\n`;
break;
default:
if (!textInputValidationAttributes.includes(key)) {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'number'.\x1b[0m`);
}
break;
}
}
} else {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'text'.\x1b[0m`);
}
});
}
// Handle the binding syntax
let bindingDirective = '';
if (attributes.binding) {
if (attributes.binding === 'bind:value' && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding.startsWith('::') && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding && !name) {
console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
return;
}
}
// Get the id from attributes or fall back to name
let id = attributes.id || name;
// Determine if semanti is true based on formSettings
const framework = this.formSettings?.framework || false;
// Construct additional attributes dynamically
let additionalAttrs = '';
for (const [key, value] of Object.entries(attributes)) {
if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
// Handle event attributes
if (framework === 'semantq') {
const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
} else {
// Add parentheses if not present
const eventValue = value.endsWith('()') ? value : `${value}()`;
additionalAttrs += ` ${key}="${eventValue}"\n`;
}
} else {
// Handle boolean attributes
if (value === true) {
additionalAttrs += ` ${key.replace(/_/g, '-')}\n`;
} else if (value !== false) {
// Convert underscores to hyphens and set the attribute
additionalAttrs += ` ${key.replace(/_/g, '-')}="${value}"\n`;
}
}
}
}
let inputClass;
if ('class' in attributes) {
inputClass = attributes.class;
} else {
inputClass = this.inputClass;
}
// Construct the final HTML string
let formHTML = `
<div class="${this.divClass}" id="${id + '-block'}">
<label for="${id}">${label}
${validationAttrs.includes('required') && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
</label>
<input
type="${type}"
name="${name}"
${bindingDirective}
id="${id}"
class="${inputClass}"
${additionalAttrs}
${validationAttrs}
${additionalAttrs.includes('placeholder') ? '' : (this.formSettings.placeholders ? `placeholder="${label}"` : '')} />
</div>
`.replace(/^\s*\n/gm, '').trim();
let formattedHtml = formHTML;
// Apply vertical layout to the <input> element only
formattedHtml = formattedHtml.replace(/<input\s+([^>]*)\/>/, (match, p1) => {
// Reformat attributes into a vertical layout
const attributes = p1.trim().split(/\s+/).map(attr => ` ${attr}`).join('\n');
return `<input\n${attributes}\n/>`;
});
this.formMarkUp +=formattedHtml;
//return formattedHtml;
}
// Specific rendering method for rendering the email field
renderEmailField(type, name, label, validate, attributes) {
// Define valid attributes for the email input type
const emailInputValidationAttributes = [
'required',
'pattern',
'minlength',
'maxlength',
'multiple'
];
// Construct validation attributes
let validationAttrs = '';
if (validate) {
Object.entries(validate).forEach(([key, value]) => {
if (emailInputValidationAttributes.includes(key)) {
if (typeof value === 'boolean' && value) {
validationAttrs += ` ${key}\n`;
} else {
switch (key) {
case 'pattern':
case 'minlength':
case 'maxlength':
validationAttrs += ` ${key}="${value}"\n`;
break;
default:
if (!emailInputValidationAttributes.includes(key)) {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'number'.\x1b[0m`);
}
break;
}
}
} else {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'email'.\x1b[0m`);
}
});
}
// Handle the binding syntax
// Handle the binding syntax
let bindingDirective = '';
if (attributes.binding) {
if (attributes.binding === 'bind:value' && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding.startsWith('::') && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding && !name) {
console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
return;
}
}
// Get the id from attributes or fall back to name
let id = attributes.id || name;
// Construct additional attributes dynamically
let additionalAttrs = '';
for (const [key, value] of Object.entries(attributes)) {
if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
// Handle event attributes
const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
} else {
// Handle boolean attributes
if (value === true) {
additionalAttrs += ` ${key.replace(/_/g, '-')}\n`;
} else if (value !== false) {
// Convert underscores to hyphens and set the attribute
additionalAttrs += ` ${key.replace(/_/g, '-')}="${value}"\n`;
}
}
}
}
let inputClass;
if ('class' in attributes) {
inputClass = attributes.class;
} else {
inputClass = this.inputClass;
}
// Construct the final HTML string
let formHTML = `
<div class="${this.divClass}" id="${id + '-block'}">
<label for="${id}">${label}
${validationAttrs.includes('required') && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
</label>
<input
type="${type}"
name="${name}"
${bindingDirective}
id="${id}"
class="${inputClass}"
${additionalAttrs}
${validationAttrs}
${additionalAttrs.includes('placeholder') ? '' : (this.formSettings.placeholders ? `placeholder="${label}"` : '')}
/>
</div>
`.replace(/^\s*\n/gm, '').trim();
let formattedHtml = formHTML;
// Apply vertical layout to the <input> element only
formattedHtml = formattedHtml.replace(/<input\s+([^>]*)\/>/, (match, p1) => {
// Reformat attributes into a vertical layout
const attributes = p1.trim().split(/\s+/).map(attr => ` ${attr}`).join('\n');
return `<input\n${attributes}\n/>`;
});
// Ensure the <div> block starts on a new line and remove extra blank lines
formattedHtml = formattedHtml.replace(/(<div\s+[^>]*>)/g, (match) => {
// Ensure <div> starts on a new line
return `\n${match}\n`;
}).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
this.formMarkUp += formattedHtml;
//return formattedHtml;
//return this.formMarkUp;
//console.log(this.formMarkUp);
}
renderNumberField(type, name, label, validate, attributes) {
// Define valid attributes for the number input type
const numberInputValidationAttributes = [
'required',
'min',
'max',
'step',
];
// Construct validation attributes
let validationAttrs = '';
if (validate) {
Object.entries(validate).forEach(([key, value]) => {
if (numberInputValidationAttributes.includes(key)) {
if (typeof value === 'boolean' && value) {
validationAttrs += ` ${key}\n`;
} else {
switch (key) {
case 'min':
case 'max':
validationAttrs += ` ${key}="${value}"\n`;
break;
case 'step':
validationAttrs += ` ${key}="${value}"\n`;
break;
default:
if (!numberInputValidationAttributes.includes(key)) {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'number'.\x1b[0m`);
}
break;
}
}
} else {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'number'.\x1b[0m`);
}
});
}
// Handle the binding syntax
let bindingDirective = '';
if (attributes.binding) {
if (attributes.binding === 'bind:value' && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding.startsWith('::') && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding && !name) {
console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
return;
}
}
// Get the id from attributes or fall back to name
let id = attributes.id || name;
// Construct additional attributes dynamically
let additionalAttrs = '';
for (const [key, value] of Object.entries(attributes)) {
if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
// Handle event attributes
const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
} else {
// Handle boolean attributes
if (value === true) {
additionalAttrs += ` ${key.replace(/_/g, '-')}\n`;
} else if (value !== false) {
// Convert underscores to hyphens and set the attribute
additionalAttrs += ` ${key.replace(/_/g, '-')}="${value}"\n`;
}
}
}
}
let inputClass;
if ('class' in attributes) {
inputClass = attributes.class;
} else {
inputClass = this.inputClass;
}
// Construct the final HTML string
let formHTML = `
<div class="${this.divClass}" id="${id + '-block'}">
<label for="${id}">${label}
${validationAttrs.includes('required') && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
</label>
<input
type="${type}"
name="${name}"
${bindingDirective}
id="${id}"
class="${inputClass}"
${additionalAttrs}
${validationAttrs}
/>
</div>
`.replace(/^\s*\n/gm, '').trim();
let formattedHtml = formHTML;
// Apply vertical layout to the <input> element only
formattedHtml = formattedHtml.replace(/<input\s+([^>]*)\/>/, (match, p1) => {
// Reformat attributes into a vertical layout
const attributes = p1.trim().split(/\s+/).map(attr => ` ${attr}`).join('\n');
return `<input\n${attributes}\n/>`;
});
// Ensure the <div> block starts on a new line and remove extra blank lines
formattedHtml = formattedHtml.replace(/(<div\s+[^>]*>)/g, (match) => {
// Ensure <div> starts on a new line
return `\n${match}\n`;
}).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
//return formattedHtml;
this.formMarkUp +=formattedHtml;
}
// New method for rendering password fields
renderPasswordField(type, name, label, validate, attributes) {
// Define valid attributes for the password input type
/*
const passwordInputAttributes = [
'required',
'minlength',
'maxlength',
'pattern',
'placeholder',
'readonly',
'disabled',
'size',
'autocomplete',
'spellcheck',
'inputmode',
'title',
];
*/
const passwordInputValidationAttributes = [
'required',
'minlength',
'maxlength',
'pattern',
];
// Construct validation attributes
let validationAttrs = '';
if (validate) {
Object.entries(validate).forEach(([key, value]) => {
if (passwordInputValidationAttributes.includes(key)) {
if (typeof value === 'boolean' && value) {
validationAttrs += ` ${key}\n`;
} else {
switch (key) {
case 'minlength':
case 'maxlength':
case 'pattern':
validationAttrs += ` ${key}="${value}"\n`;
break;
default:
if (!passwordInputValidationAttributes.includes(key)) {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'password'.\x1b[0m`);
}
break;
}
}
} else {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'password'.\x1b[0m`);
}
});
}
// Handle the binding syntax
// Handle the binding syntax
let bindingDirective = '';
if (attributes.binding) {
if (attributes.binding === 'bind:value' && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding.startsWith('::') && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding && !name) {
console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
return;
}
}
// Get the id from attributes or fall back to name
let id = attributes.id || name;
// Construct additional attributes dynamically
let additionalAttrs = '';
for (const [key, value] of Object.entries(attributes)) {
if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
// Handle event attributes
const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
} else {
// Handle boolean attributes
if (value === true) {
additionalAttrs += ` ${key.replace(/_/g, '-')}\n`;
} else if (value !== false) {
// Convert underscores to hyphens and set the attribute
additionalAttrs += ` ${key.replace(/_/g, '-')}="${value}"\n`;
}
}
}
}
let inputClass;
if ('class' in attributes) {
inputClass = attributes.class;
} else {
inputClass = this.inputClass;
}
// Construct the final HTML string
let formHTML = `
<div class="${this.divClass}" id="${id + '-block'}">
<label for="${id}">${label}
${validationAttrs.includes('required') && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
</label>
<input
type="${type}"
name="${name}"
${bindingDirective}
id="${id}"
class="${inputClass}"
${additionalAttrs}
${validationAttrs}
/>
</div>
`.replace(/^\s*\n/gm, '').trim();
let formattedHtml = formHTML;
// Apply vertical layout to the <input> element only
formattedHtml = formattedHtml.replace(/<input\s+([^>]*)\/>/, (match, p1) => {
// Reformat attributes into a vertical layout
const attributes = p1.trim().split(/\s+/).map(attr => ` ${attr}`).join('\n');
return `<input\n${attributes}\n/>`;
});
// Ensure the <div> block starts on a new line and remove extra blank lines
formattedHtml = formattedHtml.replace(/(<div\s+[^>]*>)/g, (match) => {
// Ensure <div> starts on a new line
return `\n${match}\n`;
}).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
//return formattedHtml;
this.formMarkUp +=formattedHtml;
}
// Textarea field rendering
renderTextAreaField(type, name, label, validate, attributes) {
const textInputValidationAttributes = [
'required',
'minlength',
'maxlength',
'pattern',
];
// Construct validation attributes
let validationAttrs = '';
if (validate) {
Object.entries(validate).forEach(([key, value]) => {
if (textInputValidationAttributes.includes(key)) {
if (typeof value === 'boolean' && value) {
validationAttrs += ` ${key}\n`;
} else {
switch (key) {
case 'pattern':
case 'minlength':
case 'maxlength':
validationAttrs += ` ${key}="${value}"\n`;
break;
default:
if (!textInputValidationAttributes.includes(key)) {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'number'.\x1b[0m`);
}
break;
}
}
} else {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'text'.\x1b[0m`);
}
});
}
// Handle the binding syntax
let bindingDirective = '';
if (attributes.binding) {
if (attributes.binding === 'bind:value' && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding.startsWith('::') && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding && !name) {
console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
return;
}
}
// Get the id from attributes or fall back to name
let id = attributes.id || name;
// Determine if semanti is true based on formSettings
const framework = this.formSettings?.framework || false;
// Construct additional attributes dynamically
let additionalAttrs = '';
for (const [key, value] of Object.entries(attributes)) {
if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
// Handle event attributes
if (framework === 'semantq') {
const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
} else {
// Add parentheses if not present
const eventValue = value.endsWith('()') ? value : `${value}()`;
additionalAttrs += ` ${key}="${eventValue}"\n`;
}
} else {
// Handle boolean attributes
if (value === true) {
additionalAttrs += ` ${key.replace(/_/g, '-')}\n`;
} else if (value !== false) {
// Convert underscores to hyphens and set the attribute
additionalAttrs += ` ${key.replace(/_/g, '-')}="${value}"\n`;
}
}
}
}
let inputClass;
if ('class' in attributes) {
inputClass = attributes.class;
} else {
inputClass = this.inputClass;
}
// Construct the final HTML string for textarea
let formHTML = `
<div class="${this.divClass}" id="${id + '-block'}">
<label for="${id}">${label}
${validationAttrs.includes('required') && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
</label>
<textarea
name="${name}"
${bindingDirective}
id="${id}"
class="${inputClass}"
${additionalAttrs}
${validationAttrs}
${additionalAttrs.includes('placeholder') ? '' : (this.formSettings.placeholders ? `placeholder="${label}"` : '')}>
</textarea>
</div>
`.replace(/^\s*\n/gm, '').trim();
let formattedHtml = formHTML;
// Apply vertical layout to the <textarea> element only
formattedHtml = formattedHtml.replace(/<textarea\s+([^>]*)>\s*<\/textarea>/, (match, p1) => {
// Reformat attributes into a vertical layout
const attributes = p1.trim().split(/\s+/).map(attr => ` ${attr}`).join('\n');
return `<textarea\n${attributes}\n></textarea>`;
});
this.formMarkUp += formattedHtml;
}
// New method for rendering tel fields
renderTelField(type, name, label, validate, attributes) {
// Define valid attributes for the tel input type
/*
const telInputAttributes = [
'required',
'pattern',
'placeholder',
'readonly',
'disabled',
'size',
'autocomplete',
'spellcheck',
'inputmode',
'title',
'minlength',
'maxlength',
];
*/
const telInputValidationAttributes = [
'required',
'pattern',
'minlength',
'maxlength',
];
// Construct validation attributes
let validationAttrs = '';
if (validate) {
Object.entries(validate).forEach(([key, value]) => {
if (telInputValidationAttributes.includes(key)) {
if (typeof value === 'boolean' && value) {
validationAttrs += ` ${key}\n`;
} else {
switch (key) {
case 'pattern':
case 'minlength':
case 'maxlength':
validationAttrs += ` ${key}="${value}"\n`;
break;
default:
if (!telInputValidationAttributes.includes(key)) {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'tel'.\x1b[0m`);
}
break;
}
}
} else {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'tel'.\x1b[0m`);
}
});
}
// Handle the binding syntax
let bindingDirective = '';
if (attributes.binding === 'bind:value' && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding.startsWith('::') && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding && !name) {
console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
return;
}
// Get the id from attributes or fall back to name
let id = attributes.id || name;
// Construct additional attributes dynamically
let additionalAttrs = '';
for (const [key, value] of Object.entries(attributes)) {
if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
// Handle event attributes
const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
} else {
// Handle boolean attributes
if (value === true) {
additionalAttrs += ` ${key.replace(/_/g, '-')}\n`;
} else if (value !== false) {
// Convert underscores to hyphens and set the attribute
additionalAttrs += ` ${key.replace(/_/g, '-')}="${value}"\n`;
}
}
}
}
let inputClass;
if ('class' in attributes) {
inputClass = attributes.class;
} else {
inputClass = this.inputClass;
}
// Construct the final HTML string
let formHTML = `
<div class="${this.divClass}" id="${id + '-block'}">
<label for="${id}">${label}
${validationAttrs.includes('required') && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
</label>
<input
type="${type}"
name="${name}"
${bindingDirective}
id="${id}"
class="${inputClass}"
${additionalAttrs}
${validationAttrs}
/>
</div>
`.replace(/^\s*\n/gm, '').trim();
let formattedHtml = formHTML;
// Apply vertical layout to the <input> element only
formattedHtml = formattedHtml.replace(/<input\s+([^>]*)\/>/, (match, p1) => {
// Reformat attributes into a vertical layout
const attributes = p1.trim().split(/\s+/).map(attr => ` ${attr}`).join('\n');
return `<input\n${attributes}\n/>`;
});
// Ensure the <div> block starts on a new line and remove extra blank lines
formattedHtml = formattedHtml.replace(/(<div\s+[^>]*>)/g, (match) => {
// Ensure <div> starts on a new line
return `\n${match}\n`;
}).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
return formattedHtml;
}
renderDateField(type, name, label, validate, attributes) {
// Define valid attributes for the date input type
const dateInputAttributes = [
'required',
'min',
'max',
'step',
'placeholder',
'readonly',
'disabled',
'autocomplete',
'spellcheck',
'inputmode',
'title',
];
// Construct validation attributes
let validationAttrs = '';
if (validate) {
Object.entries(validate).forEach(([key, value]) => {
if (dateInputAttributes.includes(key)) {
if (typeof value === 'boolean' && value) {
validationAttrs += ` ${key}\n`;
} else {
switch (key) {
case 'min':
case 'max':
case 'step':
validationAttrs += ` ${key}="${value}"\n`;
break;
default:
if (!dateInputAttributes.includes(key)) {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'date'.\x1b[0m`);
}
break;
}
}
} else {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type 'date'.\x1b[0m`);
}
});
}
// Handle the binding syntax
let bindingDirective = '';
if (attributes.binding === 'bind:value' && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding.startsWith('::') && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding && !name) {
console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
return;
}
// Get the id from attributes or fall back to name
let id = attributes.id || name;
// Construct additional attributes dynamically
let additionalAttrs = '';
for (const [key, value] of Object.entries(attributes)) {
if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
// Handle event attributes
const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
} else {
// Handle boolean attributes
if (value === true) {
additionalAttrs += ` ${key.replace(/_/g, '-')}\n`;
} else if (value !== false) {
// Convert underscores to hyphens and set the attribute
additionalAttrs += ` ${key.replace(/_/g, '-')}="${value}"\n`;
}
}
}
}
let inputClass;
if ('class' in attributes) {
inputClass = attributes.class;
} else {
inputClass = this.inputClass;
}
// Construct the final HTML string
let formHTML = `
<div class="${this.divClass}" id="${id + '-block'}">
<label for="${id}">${label}
${validationAttrs.includes('required') && this.formSettings.requiredFieldIndicator ? this.formSettings.asteriskHtml : ''}
</label>
<input
type="${type}"
name="${name}"
${bindingDirective}
id="${id}"
class="${inputClass}"
${additionalAttrs}
${validationAttrs}
/>
</div>
`.replace(/^\s*\n/gm, '').trim();
let formattedHtml = formHTML;
// Apply vertical layout to the <input> element only
formattedHtml = formattedHtml.replace(/<input\s+([^>]*)\/>/, (match, p1) => {
// Reformat attributes into a vertical layout
const attributes = p1.trim().split(/\s+/).map(attr => ` ${attr}`).join('\n');
return `<input\n${attributes}\n/>`;
});
// Ensure the <div> block starts on a new line and remove extra blank lines
formattedHtml = formattedHtml.replace(/(<div\s+[^>]*>)/g, (match) => {
// Ensure <div> starts on a new line
return `\n${match}\n`;
}).replace(/\n\s*\n/g, '\n'); // Remove extra blank lines
//return formattedHtml;
this.formMarkUp +=formattedHtml;
}
renderTimeField(type, name, label, validate, attributes) {
// Define valid attributes for the time input type
const timeInputAttributes = [
'required',
'min',
'max',
'step',
'readonly',
'disabled',
'autocomplete',
'spellcheck',
'inputmode',
'title',
];
// Construct validation attributes
let validationAttrs = '';
if (validate) {
Object.entries(validate).forEach(([key, value]) => {
if (timeInputAttributes.includes(key)) {
if (typeof value === 'boolean' && value) {
validationAttrs += ` ${key}\n`;
} else {
switch (key) {
case 'min':
case 'max':
case 'step':
validationAttrs += ` ${key}="${value}"\n`;
break;
default:
if (!timeInputAttributes.includes(key)) {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type '${type}'.\x1b[0m`);
}
break;
}
}
} else {
console.warn(`\x1b[31mUnsupported validation attribute '${key}' for field '${name}' of type '${type}'.\x1b[0m`);
}
});
}
// Handle the binding syntax
let bindingDirective = '';
if (attributes.binding === 'bind:value' && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding.startsWith('::') && name) {
bindingDirective = `bind:value="${name}"\n`;
}
if (attributes.binding && !name) {
console.log(`\x1b[31m%s\x1b[0m`, `You cannot set binding value when there is no name attribute defined in ${name} ${type} field.`);
return;
}
// Get the id from attributes or fall back to name
let id = attributes.id || name;
// Construct additional attributes dynamically
let additionalAttrs = '';
for (const [key, value] of Object.entries(attributes)) {
if (key !== 'id' && key !== 'class' && key !== 'dependsOn' && key !== 'dependents' && value !== undefined) { if (key.startsWith('on')) {
// Handle event attributes
const eventValue = value.endsWith('()') ? value.slice(0, -2) : value;
additionalAttrs += ` @${key.replace(/^on/, '')}={${eventValue}}\n`;
} else {
// Handle boolean attributes
if (value === true) {
additionalAttrs += ` ${key.replace(/_/g, '-')}\n`;
} else if (value !== false) {
// Convert underscores to hyphens and set the attribute
additionalAttrs += ` ${key.replace(/_/g, '-')}="${value}"\n`;
}
}
}
}
let inputClass;
if ('class' in attributes) {
inputClass = attributes.class;
} else {
inputClass