UNPKG

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
'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