Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom Checkbox-Group Control Not Working as Expected #1523

Open
Nhaqua opened this issue Feb 20, 2024 · 5 comments
Open

Custom Checkbox-Group Control Not Working as Expected #1523

Nhaqua opened this issue Feb 20, 2024 · 5 comments

Comments

@Nhaqua
Copy link

Nhaqua commented Feb 20, 2024

Hello,

I've been working on implementing a custom checkbox-group control using the formBuilder library. The goal was to extend the functionality of the existing controls to include a new control that allows users to select multiple options from a group of checkboxes. I followed the documentation and examples provided but ran into issues getting the custom control to work as expected.

Code snippet:

if (!window.fbControls) window.fbControls = [];
window.fbControls.push((controlClass) => {
  "use strict";

  class controlMXCheckboxGroup extends controlClass {
    static get definition() {
      return {
        icon: "✅",
        i18n: {
          default: "MX Checkbox Group",
          minSelectionRequired: "At least one option must be selected",
        },
      };
    }

    build() {
      const { values, ...data } = this.config;
      const options = [];
      data.name += "[]"; // Adjust name for checkbox group

      values.forEach((option, i) => {
        let optionAttributes = typeof option === "string" ? { label: option, value: option } : option;
        optionAttributes = { ...optionAttributes, type: "checkbox", id: `${data.id}-${i}`, name: data.name };

        // Construct checkbox and label elements
        const input = this.markup("input", null, optionAttributes);
        const label = this.markup("label", [input, document.createTextNode(optionAttributes.label)], {
          for: optionAttributes.id,
        });

        // Add to options array
        options.push(this.markup("div", label, { className: "formbuilder-checkbox-group" }));
      });

      this.dom = this.markup("div", options, { className: "mx-checkbox-group" });
      this.onRender = () => this.groupRequired();
      return this.dom;
    }

    groupRequired() {
      const checkboxes = this.element.getElementsByTagName("input");
      const checkValidity = () => {
        const isValid = Array.from(checkboxes).some((cb) => cb.checked);
        Array.from(checkboxes).forEach((cb) => {
          cb.setCustomValidity(isValid ? "" : this.mi18n("minSelectionRequired"));
        });
      };

      Array.from(checkboxes).forEach((cb) => cb.addEventListener("change", checkValidity));
      checkValidity(); // Initial check on render
    }
  }

  controlClass.register("mx-checkbox-group", controlMXCheckboxGroup);
});

Expected Behavior:
The custom checkbox-group should render a group of checkboxes based on the configuration provided and allow multiple selections.

Actual Behavior:
The custom control does not render correctly, and no checkboxes are displayed on the form.

Steps to Reproduce:

Add the custom control code to the formBuilder initialization script.
Try to add the 'MX Checkbox Group' control to a form.
Observe that the control does not appear as expected.
Environment Details:
formBuilder Version: 3.19.1
Browser: Chrome 121
OS: Windows 11
Could someone please assist me in troubleshooting this issue? Any guidance or suggestions would be greatly appreciated.

Thank you!

@lucasnetau
Copy link
Collaborator

You have no values array in the build stage so you have a Exception.

Uncaught TypeError: Cannot read properties of undefined (reading 'forEach')
    at controlMXCheckboxGroup.build (form_build.html:167:26)
image

Custom controls with Options is not currently supported. You can try out my WIP PR that implements such a feature #1510

@Nhaqua
Copy link
Author

Nhaqua commented Feb 21, 2024

Hello,

I've been working on implementing a custom checkbox-group control using the formBuilder library. The goal was to extend the functionality of the existing controls to include a new control that allows users to select multiple options from a group of checkboxes. I followed the documentation and examples provided but ran into issues getting the custom control to work as expected.

Code snippet:

import control from '../control'

/**
 * Text input class
 * Output a <input type="text" ... /> form element
 * @extends control
 */

export default class controlImageGroup extends control {
  static get definition() {
    return {
      icon: '♥',
      i18n: {
        default: 'Image Group',
      },
      inactive: ['checkbox'],
      mi18n: {
        minSelectionRequired: 'minSelectionRequired',
      },
    }
  }
  /**
   * build a select DOM element, supporting other jquery text form-control's
   * @return {Object} DOM Element to be injected into the form.
   */
  config = { values: [{ label: 'Option 1' }, { label: 'Option 2' }] }
  build() {
    const options = []
    const { values, value, placeholder, type, inline, toggle, ...data } = this.config

    const optionType = 'checkbox'
    if (data.multiple || type === 'image-group') {
      data.name = data.name + '[]'
    }

    if (type === 'image-group' && data.required) {
      const self = this
      const defaultOnRender = this.onRender.bind(this)
      this.onRender = function () {
        defaultOnRender()
        self.groupRequired()
      }
    }

    delete data.title

    if (values) {
      // if a placeholder is specified, add it to the top of the option list

      // process the rest of the options
      for (let i = 0; i < values.length; i++) {
        let option = values[i]
        if (typeof option === 'string') {
          option = { label: option, value: option }
        }
        const { label = '', ...optionAttrs } = option
        optionAttrs.id = `${data.id}-${i}`

        // don't select this option if a placeholder is defined
        if (!optionAttrs.selected || placeholder) {
          delete optionAttrs.selected
        }

        // if a value is defined at select level, select this attribute
        if (typeof value !== 'undefined' && optionAttrs.value === value) {
          optionAttrs.selected = true
        }

        const labelContents = [label]
        let wrapperClass = `formbuilder-${optionType}`
        if (inline) {
          wrapperClass += '-inline'
        }
        optionAttrs.type = optionType
        if (optionAttrs.selected) {
          optionAttrs.checked = 'checked'
          delete optionAttrs.selected
        }
        const input = this.markup('input', null, Object.assign({}, data, optionAttrs))
        const labelAttrs = { for: optionAttrs.id }
        let output = [input, this.markup('label', labelContents, labelAttrs)]
        if (toggle) {
          labelAttrs.className = 'kc-toggle'
          labelContents.unshift(input, this.markup('span'))
          output = this.markup('label', labelContents, labelAttrs)
        }

        const wrapper = this.markup('div', output, { className: wrapperClass })
        options.push(wrapper)
      }

      const otherOptionAttrs = {
        id: `${data.id}-other`,
        className: `${data.className ?? ''} other-option`,
        value: '',
      }

      let wrapperClass = `formbuilder-${optionType}`
      if (inline) {
        wrapperClass += '-inline'
      }

      const optionAttrs = Object.assign({}, data, otherOptionAttrs)
      optionAttrs.type = optionType

      const otherValAttrs = {
        type: 'text',
        events: {
          input: evt => {
            const otherInput = evt.target
            const other = otherInput.parentElement.previousElementSibling
            other.value = otherInput.value
          },
        },
        id: `${otherOptionAttrs.id}-value`,
        className: 'other-val',
      }
      const primaryInput = this.markup('input', null, optionAttrs)
      const otherInputs = [document.createTextNode(control.mi18n('other')), this.markup('input', null, otherValAttrs)]
      const inputLabel = this.markup('label', otherInputs, { for: optionAttrs.id })
      const wrapper = this.markup('div', [primaryInput, inputLabel], { className: wrapperClass })
      options.push(wrapper)
    }

    // build & return the DOM elements

    this.dom = this.markup('div', options, { className: type })

    return this.dom
  }

  /**
   * setCustomValidity for image-group
   */
  groupRequired() {
    const checkboxes = this.element.getElementsByTagName('input')
    const setValidity = (checkbox, isValid) => {
      const minReq = control.mi18n('minSelectionRequired', 1)
      if (!isValid) {
        checkbox.setCustomValidity(minReq)
      } else {
        checkbox.setCustomValidity('')
      }
    }
    const toggleRequired = (checkboxes, isValid) => {
      ;[].forEach.call(checkboxes, cb => {
        if (isValid) {
          cb.removeAttribute('required')
        } else {
          cb.setAttribute('required', 'required')
        }
        setValidity(cb, isValid)
      })
    }

    const toggleValid = () => {
      const isValid = [].some.call(checkboxes, cb => cb.checked)
      toggleRequired(checkboxes, isValid)
    }

    for (let i = checkboxes.length - 1; i >= 0; i--) {
      checkboxes[i].addEventListener('change', toggleValid)
    }
    toggleValid()
  }

  /**
   * onRender callback
   */
  onRender() {
    if (this.config.userData) {
      const selectedOptions = this.config.userData.slice()

      if (this.config.type.endsWith('-group')) {
        if (this.config.type === 'image-group') {
          this.dom.querySelectorAll('input[type=checkbox]').forEach(input => {
            input.removeAttribute('checked')
          })
        }
        this.dom.querySelectorAll('input').forEach(input => {
          if (input.classList.contains('other-val')) {
            return
          }

          for (let i = 0; i < selectedOptions.length; i++) {
            if (input.value === selectedOptions[i]) {
              input.setAttribute('checked', 'checked')
              selectedOptions.splice(i, 1)
              break
            }
          }
          if (input.id.endsWith('-other') && selectedOptions.length > 0) {
            const otherVal = this.dom.querySelector(`#${input.id}-value`)
            input.setAttribute('checked', 'checked')
            otherVal.style.display = 'inline-block'
          }
        })
      }
    }
  }
}

control.register('image-group', controlImageGroup)

Screenshot 2024-02-21 141933
However, when editing, it doesn't have the options.
image

Could someone please assist me in troubleshooting this issue? Any guidance or suggestions would be greatly appreciated.

Thank you!

@lucasnetau
Copy link
Collaborator

Please read the original response given to you.

@Nhaqua
Copy link
Author

Nhaqua commented Feb 21, 2024

@lucasnetau
Can you help me create an example?
Thank you!

@lucasnetau
Copy link
Collaborator

There is an example in the linked PR #1510

Documentation for the options feature has yet to be written which is why it is not merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants