Jake Vanderwerf
2026-01-20 7a9054bb3f033c98067b3196378311dae54c5fbf
assets/js/concise/FormController.js
@@ -320,8 +320,6 @@
      }
      let form = this.getForm(e.target);
      //Autosave
      if (!form || !form.options.cache) return;
      this.updateItem(field.dataset.field, this.getFieldValue(e.target), form);
   }
@@ -335,9 +333,7 @@
      window.debouncer.cancel(`form:${form.id}:validate:${fieldName}`);
      this.validateField(e.target);
      if (form.options.cache) {
         this.updateItem(fieldName, this.getFieldValue(e.target), form);
      }
      this.updateItem(fieldName, this.getFieldValue(e.target), form);
   }
   handleInput(e){
@@ -363,10 +359,18 @@
         e.preventDefault();
         const storedData = await this.store.get(form.id);
         this.notify('form-submit', {
            config: form,
            data: storedData?.changes || {}
         });
         if (form.options.cache) {
            this.notify('form-submit', {
               config: form,
               data: storedData.changes
            });
         } else {
            this.notify('form-submit', {
               config: form,
               data: this.changes.get(form.id)?.changes??{},
            });
         }
      }
      if (form.options.showSummary) {
@@ -396,7 +400,9 @@
      let changes = this.changes.get(form.id);
      changes.changes[name] = value;
      this.changes.set(form.id, changes);
      this.scheduleBackup();
      if (form.options.cache) {
         this.scheduleBackup();
      }
   }
   scheduleBackup()  {
@@ -1194,6 +1200,146 @@
      }
   }
   handleFormSuccess(form, data) {
      // Clear previous errors
      form.querySelectorAll('.error-message').forEach(el => el.remove());
      form.querySelectorAll('.field-error').forEach(el =>
         el.classList.remove('field-error')
      );
      // Add success class to form
      form.classList.add('form-success');
      // Show success message if provided
      if (data.message) {
         const success = document.createElement('div');
         success.className = 'form-success-message success-message';
         success.textContent = data.message;
         form.insertBefore(success, form.firstChild);
         const icon = window.getIcon?.('check-circle');
         if (icon) {
            icon.classList.add('success-icon');
            success.prepend(icon);
         }
      }
      // If there's a title/description (for registration success)
      if (data.title || data.description) {
         const successBox = document.createElement('div');
         successBox.className = 'success-box';
         if (data.title) {
            const title = document.createElement('h3');
            title.textContent = data.title;
            successBox.appendChild(title);
         }
         if (data.description) {
            const descriptions = Array.isArray(data.description)
               ? data.description
               : [data.description];
            descriptions.forEach(desc => {
               const p = document.createElement('p');
               p.textContent = desc;
               successBox.appendChild(p);
            });
         }
         form.insertBefore(successBox, form.firstChild);
      }
      //  DELETE CACHED FORM DATA ON SUCCESS
      if (form.dataset.formId) {
         this.store.delete(form.dataset.formId).catch(err => {
            console.warn('Failed to clear form cache:', err);
         });
         // Clear form config dirty state
         const formConfig = this.forms.get(form.dataset.formId);
         if (formConfig) {
            formConfig.isDirty = false;
            formConfig.lastSaved = Date.now();
            formConfig.data = {}; // Clear cached data
         }
      }
      // Announce success for accessibility
      if (window.jvbA11y) {
         window.jvbA11y.announce(data.message || 'Form submitted successfully');
      }
      // Trigger custom event
      form.dispatchEvent(new CustomEvent('jvb-form-success', {
         detail: data
      }));
   }
   handleFormError(form, data) {
      // Clear all previous errors
      form.querySelectorAll('.error-message').forEach(el => el.remove());
      form.querySelectorAll('.field-error, .has-error').forEach(el => {
         el.classList.remove('field-error', 'has-error');
      });
      // Clear validation states using existing method
      form.querySelectorAll('.field').forEach(fieldWrapper => {
         this.clearValidation(fieldWrapper);
      });
      // Handle field-specific errors
      if (data.field) {
         const fieldWrapper = form.querySelector(`[data-field="${data.field}"]`);
         if (fieldWrapper) {
            // Use existing showError method for consistency
            this.showError(fieldWrapper, data.message);
            // Mark as touched so validation persists
            this.touchedFields.add(data.field);
            // Scroll to error
            fieldWrapper.scrollIntoView({ behavior: 'smooth', block: 'center' });
            // Focus the input for better UX
            const input = fieldWrapper.querySelector('input, textarea, select');
            if (input) {
               input.focus();
            }
         }
      } else {
         // General form error (not field-specific)
         const error = document.createElement('div');
         error.className = 'form-error error-message';
         error.textContent = data.message;
         // Add icon for consistency
         const icon = window.getIcon?.('close-circle');
         if (icon) {
            icon.classList.add('error-icon');
            error.prepend(icon);
         }
         form.insertBefore(error, form.firstChild);
         // Scroll to top to show the error
         form.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }
      // Announce error for accessibility
      if (window.jvbA11y) {
         const announcement = data.field
            ? `Error in ${data.field}: ${data.message}`
            : `Form error: ${data.message}`;
         window.jvbA11y.announce(announcement);
      }
      // Trigger custom event
      form.dispatchEvent(new CustomEvent('jvb-form-error', {
         detail: data
      }));
   }
   /**********************************************************************
   STATUS
    **********************************************************************/
@@ -1375,6 +1521,23 @@
      this.inputs.set(config.id, config);
   }
   /**********************************************************************
    Subscription
   **********************************************************************/
   subscribe(callback) {
      this.subscribers.add(callback);
      return () => this.subscribers.delete(callback);
   }
   notify(event, data) {
      this.subscribers.forEach(cb => {
         try {
            cb(event, data);
         } catch (e) {
            console.error('HandleSelection subscriber error:', e);
         }
      });
   }
   /**********************************************************************
    Cleanup
   **********************************************************************/
   destroy() {